Branch data Line data Source code
1 : : #include "precizer.h"
2 : : #include <sysexits.h>
3 : :
4 : : /* Shared CLI option table */
5 : : extern const struct argp_option options[];
6 : :
7 : : /**
8 : : * @brief Share record handling results with the file reader through inih's user pointer
9 : : *
10 : : * One local instance belongs to each read_config_file() call. has_entries
11 : : * records whether a recognized, permitted key was encountered, including a
12 : : * false flag that disables an option. status records handler failure, so the
13 : : * reader does not add a generic syntax error after a diagnostic was already
14 : : * reported. state supplies the program name and error stream for diagnostics.
15 : : * The settings themselves are stored in Config
16 : : */
17 : : typedef struct {
18 : : bool has_entries;
19 : : Return status;
20 : : struct argp_state *state;
21 : : } ConfigFileReadContext;
22 : :
23 : : /**
24 : : * @brief Resolve a configuration key and apply its value through parse_opt
25 : : *
26 : : * Strip one matching pair of outer quotes before interpreting the value.
27 : : * Interior spaces and backslashes are preserved after inih handles comments
28 : : *
29 : : * @param user Configuration read context
30 : : * @param section Ignored section name supplied by inih
31 : : * @param name Exact long option name, or NULL for a section header
32 : : * @param value Parsed value, valid only during this call
33 : : * @param line Original physical line number
34 : : * @return Nonzero when a setting is applied or a section is ignored, or zero after an error
35 : : */
36 : 981 : static int read_config_entry(
37 : : void *user,
38 : : const char *section __attribute__((unused)),
39 : : const char *name,
40 : : const char *value,
41 : : int line)
42 : : {
43 : 981 : ConfigFileReadContext *context = user;
44 : :
45 : : /* Section headers only organize the file; all keys share the same settings */
46 [ + + ]: 981 : if(name == NULL)
47 : : {
48 : 10 : return(1);
49 : : }
50 : :
51 : : /* Help, usage, version, and configuration file selection are available only
52 : : through CLI. Reject these keys before parsing their values, so even
53 : : help = false is an error. The lookup below accepts only exact long names
54 : : without leading dashes; short forms such as h, V, and -h are rejected
55 : : as unknown keys */
56 [ + + + + ]: 971 : if(strcmp(name,"help") == 0 || strcmp(name,"usage") == 0 ||
57 [ + + + + ]: 969 : strcmp(name,"version") == 0 || strcmp(name,"config") == 0)
58 : : {
59 : 4 : argp_failure(context->state,0,0,"ERROR: Line %d in config file: Config key '%s' is only available in CLI",line,name);
60 : 4 : context->status = FAILURE;
61 : 4 : return(0);
62 : : }
63 : :
64 : : /* For an option entry, argp_option.name is the long CLI name, key selects
65 : : its parse_opt() branch, and arg names the expected value (NULL for a flag).
66 : : This pointer refers to an existing options[] entry without copying it.
67 : : Keep the most recent non-alias entry so an alias can use its primary
68 : : option's value requirements and handler key */
69 : 967 : const struct argp_option *option = NULL;
70 : :
71 : : /* Find this configuration key in the shared CLI table. Stop at the zero
72 : : terminator, not at the first entry without a name: help group headings
73 : : and aliases that provide only a short option also have NULL names.
74 : : Compare the supplied key with each long name exactly, including case.
75 : : On a match, convert flag values and apply the setting, then return.
76 : : Reaching the end without a match produces the unknown-key error below */
77 [ + + ]: 4228 : for(size_t i = 0; options[i].name != NULL || options[i].key != 0 ||
78 [ + + + + : 20932 : options[i].doc != NULL || options[i].group != 0; i++)
- + ]
79 : : {
80 : : /* Aliases inherit the argument requirements of their primary option.
81 : : Match the alias's own name below while retaining the primary entry */
82 [ + + ]: 17671 : if((options[i].flags & OPTION_ALIAS) == 0)
83 : : {
84 : 15379 : option = &options[i];
85 : : }
86 : :
87 [ + + + + ]: 17671 : if(options[i].name != NULL && strcmp(name,options[i].name) == 0)
88 : : {
89 : : /* A false flag still counts as an explicit setting */
90 : 962 : context->has_entries = true;
91 : :
92 : : /* Whether parse_opt() needs the prepared value as its argument.
93 : : Remains true for options with values and for false flags, which
94 : : receive the normalized string "false". Becomes false for true
95 : : flags, which receive NULL just like flags supplied through CLI */
96 : 962 : bool takes_value = true;
97 : :
98 : : /* Number of value bytes to copy, excluding the terminating null byte.
99 : : Initially includes any outer quotes; reduced by two when they are removed.
100 : : inih has already removed surrounding whitespace and comments.
101 : : Only a leading single or double quote starts a quoted value;
102 : : otherwise the pointer and length retain the entire value */
103 : 962 : size_t value_length = strlen(value);
104 : :
105 [ + + + + ]: 962 : if(value[0] == '"' || value[0] == '\'')
106 : : {
107 : : /* A quoted value needs at least two characters, and its final
108 : : character must match the opening quote. A lone opening quote
109 : : or a different final character is an error on this physical line */
110 [ + + + + ]: 32 : if(value_length < 2 || value[value_length - 1] != value[0])
111 : : {
112 : : /* Report the key and line, then stop reading. Recording the failure
113 : : prevents the file reader from adding a generic syntax error */
114 : 4 : argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unclosed quoted value for '%s'",line,name);
115 : :
116 : 4 : context->status = FAILURE;
117 : :
118 : 4 : return(0);
119 : : }
120 : :
121 : : /* Skip the opening quote and exclude both quotes from the copy length.
122 : : The original text is not modified. Only this outer pair is removed;
123 : : inner quotes, spaces, and backslashes remain literal characters.
124 : : An empty pair produces a zero-length value for normal option validation */
125 : 28 : value++;
126 : :
127 : 28 : value_length -= 2;
128 : : }
129 : :
130 : : /* Argp accepts mutable arguments, so copy the selected bytes into a local
131 : : buffer. inih bounds the input with INI_MAX_LINE, so the value fits.
132 : : Add the terminating null byte explicitly: memcpy() copies only the
133 : : selected contents, excluding any closing quote and original terminator */
134 : : char value_buffer[INI_MAX_LINE];
135 : :
136 : 958 : memcpy(value_buffer,value,value_length);
137 : :
138 : 958 : value_buffer[value_length] = '\0';
139 : :
140 : : /* Interpret booleans and other option values from this prepared copy,
141 : : so quoted and unquoted spellings reach the same validation logic */
142 : 958 : value = value_buffer;
143 : :
144 : : /* Flags accept boolean values, except compare, whose file entries
145 : : contain database paths. dry-run also accepts a named mode,
146 : : which is validated by parse_opt() when it is not a boolean */
147 [ + + + + : 958 : if((option->arg == NULL && option->key != 'c') || option->key == 'n')
+ + ]
148 : : {
149 : : /* Normalize false spellings for the shared option handler */
150 [ + + + + ]: 408 : if(strcasecmp(value,"false") == 0 || strcasecmp(value,"no") == 0 ||
151 [ + + + + ]: 334 : strcasecmp(value,"off") == 0 || strcasecmp(value,"n") == 0 ||
152 [ + + ]: 285 : strcmp(value,"0") == 0)
153 : : {
154 : 149 : strcpy(value_buffer,"false");
155 : :
156 [ + + + + ]: 259 : } else if(strcasecmp(value,"true") == 0 || strcasecmp(value,"yes") == 0 ||
157 [ + + + + ]: 60 : strcasecmp(value,"on") == 0 || strcasecmp(value,"y") == 0 ||
158 [ + + ]: 36 : strcmp(value,"1") == 0){
159 : : /* A true flag reaches parse_opt() without a value, as in CLI */
160 : 237 : takes_value = false;
161 : :
162 [ + + ]: 22 : } else if(option->key != 'n'){
163 : :
164 : 6 : argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unsupported --%s value '%s'. Supported values: true/yes/on/y/1 or false/no/off/n/0",line,name,value);
165 : 6 : context->status = FAILURE;
166 : 6 : return(0);
167 : : }
168 : : }
169 : :
170 : 952 : char *argument = NULL;
171 : :
172 [ + + ]: 952 : if(takes_value == true)
173 : : {
174 : 715 : argument = value_buffer;
175 : : }
176 : :
177 : : /* Each compare entry enables comparison and supplies one database
178 : : path through the existing positional-argument handler */
179 : 952 : int key = option->key;
180 : :
181 [ + + ]: 952 : if(key == 'c')
182 : : {
183 [ + + ]: 85 : if(argument[0] == '\0')
184 : : {
185 : 4 : argp_failure(context->state,0,0,"ERROR: Line %d in config file: Database path for 'compare' must not be empty",line);
186 : 4 : context->status = FAILURE;
187 : 4 : return(0);
188 : : }
189 : :
190 : 81 : config->compare = true;
191 : 81 : key = ARGP_KEY_ARG;
192 : : }
193 : :
194 : : /* Make the line available to shared option diagnostics only while this
195 : : entry is being applied. Reset it even when parse_opt() returns an
196 : : error, so later CLI processing cannot inherit a file line */
197 : 948 : config->config_file_error_line = line;
198 : :
199 : 948 : error_t parse_error = parse_opt(key,argument,context->state);
200 : :
201 : 948 : config->config_file_error_line = -1;
202 : :
203 [ + + ]: 948 : if(parse_error != EX_OK)
204 : : {
205 : 35 : context->status = FAILURE;
206 : 35 : return(0);
207 : : }
208 : :
209 : 913 : return(1);
210 : : }
211 : : }
212 : :
213 : 5 : argp_failure(context->state,0,0,"ERROR: Line %d in config file: Unknown config key '%s'",line,name);
214 : 5 : context->status = FAILURE;
215 : 5 : return(0);
216 : : }
217 : :
218 : : /**
219 : : * @brief Read configuration with inih and apply options through parse_opt
220 : : *
221 : : * Config must be initialized before reading. Recognized records are applied
222 : : * in file order regardless of section. Section headers are ignored, and
223 : : * reading stops at the first syntax, name, or value error.
224 : : * Required parameters and option combinations are checked during finalization
225 : : *
226 : : * @param path Configuration file path, relative to the working directory or absolute
227 : : * @param required Whether a missing file is an error; other open errors always fail
228 : : * @param has_entries Set when at least one recognized, permitted record is read
229 : : * @param state Diagnostic state with the program name and error stream
230 : : * @return SUCCESS on completion or an optional missing file, otherwise FAILURE
231 : : */
232 : 276 : Return read_config_file(
233 : : const char *path,
234 : : bool required,
235 : : bool *has_entries,
236 : : struct argp_state *state)
237 : : {
238 : : /* Status returned by this function through deliver()
239 : : Default value assumes successful completion */
240 : 276 : Return status = SUCCESS;
241 : :
242 : : /* Output flag for whether a recognized, permitted setting key was read.
243 : : Blank lines, comments, and section headers do not count; false flags do.
244 : : This distinguishes a file without settings from one that leaves defaults
245 : : unchanged. Start with no entries, including when an optional file is absent.
246 : : The return status reports errors separately; this flag does not mean success */
247 : 276 : *has_entries = false;
248 : :
249 : 276 : FILE *file = fopen(path,"rb");
250 : :
251 [ + + ]: 276 : if(file == NULL)
252 : : {
253 [ + + + + ]: 14 : if(required || errno != ENOENT)
254 : : {
255 : 11 : argp_failure(state,0,errno,"ERROR: Cannot open config file '%s'",path);
256 : 11 : status = FAILURE;
257 : : }
258 : :
259 : : } else {
260 : :
261 : : /* inih reads the open file and calls read_config_entry() for each record.
262 : : The callback applies settings and records any error it reports.
263 : : Copy its status and setting-presence flag back after parsing.
264 : : If the callback succeeded, check for a stream read error first, then
265 : : a parser error, using the returned line number for syntax diagnostics.
266 : : The status checks prevent a second message for an already reported error */
267 : 262 : ConfigFileReadContext context = {.status = SUCCESS,.state = state};
268 : :
269 : 262 : int parse_error = ini_parse_file(file,read_config_entry,&context);
270 : :
271 : 262 : status = context.status;
272 : :
273 : 262 : *has_entries = context.has_entries;
274 : :
275 [ + + + + ]: 262 : if(SUCCESS == status && ferror(file))
276 : : {
277 : 7 : argp_failure(state,0,errno,"ERROR: Cannot read config file '%s'",path);
278 : 7 : status = FAILURE;
279 : : }
280 : :
281 [ + + + + ]: 262 : if(SUCCESS == status && parse_error != 0)
282 : : {
283 : 4 : argp_failure(state,0,0,"ERROR: Line %d in config file: Invalid config syntax",parse_error);
284 : 4 : status = FAILURE;
285 : : }
286 : :
287 : : /* Closing remains mandatory after a parse or read error */
288 : 262 : int close_error = fclose(file);
289 : :
290 [ + + - + ]: 262 : if(SUCCESS == status && close_error != 0)
291 : : {
292 : 0 : argp_failure(state,0,errno,"ERROR: Cannot close config file '%s'",path);
293 : 0 : status = FAILURE;
294 : : }
295 : : }
296 : :
297 : 276 : deliver(status);
298 : : }
|