UNIXworkcode

1 /* 2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3 * 4 * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 #include "cx/properties.h" 30 31 #include <assert.h> 32 #include <stdio.h> 33 #include <string.h> 34 #include <ctype.h> 35 36 const CxPropertiesConfig cx_properties_config_default = { 37 '=', 38 '#', 39 '\0', 40 '\0', 41 '\\', 42 }; 43 44 void cxPropertiesInit( 45 CxProperties *prop, 46 CxPropertiesConfig config 47 ) { 48 memset(prop, 0, sizeof(CxProperties)); 49 prop->config = config; 50 } 51 52 void cxPropertiesDestroy(CxProperties *prop) { 53 cxBufferDestroy(&prop->input); 54 cxBufferDestroy(&prop->buffer); 55 } 56 57 void cxPropertiesReset(CxProperties *prop) { 58 CxPropertiesConfig config = prop->config; 59 cxPropertiesDestroy(prop); 60 cxPropertiesInit(prop, config); 61 } 62 63 int cxPropertiesFilln( 64 CxProperties *prop, 65 const char *buf, 66 size_t len 67 ) { 68 if (cxBufferEof(&prop->input)) { 69 // destroy a possible previously initialized buffer 70 cxBufferDestroy(&prop->input); 71 cxBufferInit(&prop->input, (void*) buf, len, 72 NULL, CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_AUTO_EXTEND); 73 prop->input.size = len; 74 } else { 75 if (cxBufferAppend(buf, 1, len, &prop->input) < len) return -1; 76 } 77 return 0; 78 } 79 80 void cxPropertiesUseStack( 81 CxProperties *prop, 82 char *buf, 83 size_t capacity 84 ) { 85 cxBufferInit(&prop->buffer, buf, capacity, NULL, CX_BUFFER_COPY_ON_EXTEND); 86 } 87 88 CxPropertiesStatus cxPropertiesNext( 89 CxProperties *prop, 90 cxstring *key, 91 cxstring *value 92 ) { 93 // check if we have a text buffer 94 if (prop->input.space == NULL) { 95 return CX_PROPERTIES_NULL_INPUT; 96 } 97 98 // a pointer to the buffer we want to read from 99 CxBuffer *current_buffer = &prop->input; 100 101 char comment1 = prop->config.comment1; 102 char comment2 = prop->config.comment2; 103 char comment3 = prop->config.comment3; 104 char delimiter = prop->config.delimiter; 105 char continuation = prop->config.continuation; 106 107 // check if we have rescued data 108 if (!cxBufferEof(&prop->buffer)) { 109 // check if we can now get a complete line 110 cxstring input = cx_strn(prop->input.space + prop->input.pos, 111 prop->input.size - prop->input.pos); 112 cxstring nl = cx_strchr(input, '\n'); 113 while (nl.length > 0) { 114 // check for line continuation 115 char previous = nl.ptr > input.ptr ? nl.ptr[-1] : prop->buffer.space[prop->buffer.size-1]; 116 if (previous == continuation) { 117 // this nl is a line continuation, check the next newline 118 nl = cx_strchr(cx_strsubs(nl, 1), '\n'); 119 } else { 120 break; 121 } 122 } 123 124 if (nl.length > 0) { 125 // we add as much data to the rescue buffer as we need 126 // to complete the line 127 size_t len_until_nl = (size_t)(nl.ptr - input.ptr) + 1; 128 129 if (cxBufferAppend(input.ptr, 1, 130 len_until_nl, &prop->buffer) < len_until_nl) { 131 return CX_PROPERTIES_BUFFER_ALLOC_FAILED; 132 } 133 134 // advance the position in the input buffer 135 prop->input.pos += len_until_nl; 136 137 // we now want to read from the rescue buffer 138 current_buffer = &prop->buffer; 139 } else { 140 // still not enough data, copy input buffer to internal buffer 141 if (cxBufferAppend(input.ptr, 1, 142 input.length, &prop->buffer) < input.length) { 143 return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE 144 } 145 // reset the input buffer (make way for a re-fill) 146 cxBufferReset(&prop->input); 147 return CX_PROPERTIES_INCOMPLETE_DATA; 148 } 149 } 150 151 // get one line and parse it 152 while (!cxBufferEof(current_buffer)) { 153 const char *buf = current_buffer->space + current_buffer->pos; 154 size_t len = current_buffer->size - current_buffer->pos; 155 156 /* 157 * First we check if we have at least one line. We also get indices of 158 * delimiter and comment chars 159 */ 160 size_t delimiter_index = 0; 161 size_t comment_index = 0; 162 bool has_comment = false; 163 bool has_continuation = false; 164 165 size_t i = 0; 166 char c = 0; 167 for (; i < len; i++) { 168 c = buf[i]; 169 if (c == comment1 || c == comment2 || c == comment3) { 170 if (comment_index == 0) { 171 comment_index = i; 172 has_comment = true; 173 } 174 } else if (c == delimiter) { 175 if (delimiter_index == 0 && !has_comment) { 176 delimiter_index = i; 177 } 178 } else if (delimiter_index > 0 && c == continuation && i+1 < len && buf[i+1] == '\n') { 179 has_continuation = true; 180 i++; 181 } else if (c == '\n') { 182 break; 183 } 184 } 185 186 if (c != '\n') { 187 // we don't have enough data for a line, use the rescue buffer 188 assert(current_buffer != &prop->buffer); 189 // make sure that the rescue buffer does not already contain something 190 assert(cxBufferEof(&prop->buffer)); 191 if (prop->buffer.space == NULL) { 192 // initialize a rescue buffer, if the user did not provide one 193 cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND); 194 } else { 195 // from a previous rescue there might be already read data 196 // reset the buffer to avoid unnecessary buffer extension 197 cxBufferReset(&prop->buffer); 198 } 199 if (cxBufferAppend(buf, 1, len, &prop->buffer) < len) { 200 return CX_PROPERTIES_BUFFER_ALLOC_FAILED; 201 } 202 // reset the input buffer (make way for a re-fill) 203 cxBufferReset(&prop->input); 204 return CX_PROPERTIES_INCOMPLETE_DATA; 205 } 206 207 cxstring line = has_comment ? 208 cx_strn(buf, comment_index) : 209 cx_strn(buf, i); 210 // check line 211 if (delimiter_index == 0) { 212 // if line is not blank ... 213 line = cx_strtrim(line); 214 // ... either no delimiter found, or key is empty 215 if (line.length > 0) { 216 if (line.ptr[0] == delimiter) { 217 return CX_PROPERTIES_INVALID_EMPTY_KEY; 218 } else { 219 return CX_PROPERTIES_INVALID_MISSING_DELIMITER; 220 } 221 } else { 222 // skip blank line 223 // if it was the rescue buffer, return to the original buffer 224 if (current_buffer == &prop->buffer) { 225 // assert that the rescue buffer really does not contain more data 226 assert(current_buffer->pos + i + 1 == current_buffer->size); 227 // reset the rescue buffer, but don't destroy it! 228 cxBufferReset(&prop->buffer); 229 // continue with the input buffer 230 current_buffer = &prop->input; 231 } else { 232 // if it was the input buffer already, just advance the position 233 current_buffer->pos += i + 1; 234 } 235 continue; 236 } 237 } else { 238 cxstring k = cx_strn(buf, delimiter_index); 239 cxstring val = cx_strn( 240 buf + delimiter_index + 1, 241 line.length - delimiter_index - 1); 242 k = cx_strtrim(k); 243 val = cx_strtrim(val); 244 if (k.length > 0) { 245 current_buffer->pos += i + 1; 246 assert(current_buffer->pos <= current_buffer->size); 247 assert(current_buffer != &prop->buffer || current_buffer->pos == current_buffer->size); 248 249 if (has_continuation) { 250 char *ptr = (char*)val.ptr; 251 if (current_buffer != &prop->buffer) { 252 // move value to the rescue buffer 253 if (prop->buffer.space == NULL) { 254 cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND); 255 } 256 prop->buffer.size = 0; 257 prop->buffer.pos = 0; 258 if (cxBufferWrite(val.ptr, 1, val.length, &prop->buffer) != val.length) { 259 return CX_PROPERTIES_BUFFER_ALLOC_FAILED; 260 } 261 val.ptr = prop->buffer.space; 262 ptr = prop->buffer.space; 263 } 264 // value.ptr is now inside the rescue buffer and we can 265 // remove the continuation character from the value 266 bool trim = false; 267 size_t x = 0; 268 for(size_t j=0;j<val.length;j++) { 269 c = ptr[j]; 270 if (j+1 < val.length && c == '\\' && ptr[j+1] == '\n') { 271 // skip continuation and newline character 272 j++; 273 trim = true; // enable trim in the next line 274 continue; 275 } 276 if (j > x) { 277 if (trim) { 278 if (isspace((unsigned char)c)) { 279 continue; 280 } 281 trim = false; 282 } 283 ptr[x] = c; 284 } 285 x++; 286 } 287 val.length = x; 288 } 289 *key = k; 290 *value = val; 291 292 return CX_PROPERTIES_NO_ERROR; 293 } else { 294 return CX_PROPERTIES_INVALID_EMPTY_KEY; 295 } 296 } 297 } 298 299 // when we come to this point, all data must have been read 300 assert(cxBufferEof(&prop->buffer)); 301 assert(cxBufferEof(&prop->input)); 302 303 return CX_PROPERTIES_NO_DATA; 304 } 305 306 #ifndef CX_PROPERTIES_LOAD_FILL_SIZE 307 #define CX_PROPERTIES_LOAD_FILL_SIZE 1024 308 #endif 309 const unsigned cx_properties_load_fill_size = CX_PROPERTIES_LOAD_FILL_SIZE; 310 #ifndef CX_PROPERTIES_LOAD_BUF_SIZE 311 #define CX_PROPERTIES_LOAD_BUF_SIZE 256 312 #endif 313 const unsigned cx_properties_load_buf_size = CX_PROPERTIES_LOAD_BUF_SIZE; 314 315 CxPropertiesStatus cx_properties_load(CxPropertiesConfig config, 316 const CxAllocator *allocator, cxstring filename, CxMap *target) { 317 if (allocator == NULL) { 318 allocator = cxDefaultAllocator; 319 } 320 321 // sanity check for the map 322 const bool use_cstring = cxCollectionStoresPointers(target); 323 if (!use_cstring && cxCollectionElementSize(target) != sizeof(cxmutstr)) { 324 return CX_PROPERTIES_MAP_ERROR; 325 } 326 327 // create a duplicate to guarantee zero-termination 328 cxmutstr fname = cx_strdup(filename); 329 if (fname.ptr == NULL) { 330 return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE 331 } 332 333 // open the file 334 FILE *f = fopen(fname.ptr, "r"); 335 if (f == NULL) { 336 cx_strfree(&fname); 337 return CX_PROPERTIES_FILE_ERROR; 338 } 339 340 // initialize the parser 341 char linebuf[cx_properties_load_buf_size]; 342 char fillbuf[cx_properties_load_fill_size]; 343 CxPropertiesStatus status; 344 CxProperties parser; 345 cxPropertiesInit(&parser, config); 346 cxPropertiesUseStack(&parser, linebuf, cx_properties_load_buf_size); 347 348 // read/fill/parse loop 349 status = CX_PROPERTIES_NO_DATA; 350 size_t keys_found = 0; 351 while (true) { 352 size_t r = fread(fillbuf, 1, cx_properties_load_fill_size, f); 353 if (ferror(f)) { 354 status = CX_PROPERTIES_FILE_ERROR; 355 break; 356 } 357 if (r == 0) { 358 break; 359 } 360 if (cxPropertiesFilln(&parser, fillbuf, r)) { 361 status = CX_PROPERTIES_BUFFER_ALLOC_FAILED; 362 break; 363 } 364 cxstring key, value; 365 while (true) { 366 status = cxPropertiesNext(&parser, &key, &value); 367 if (status != CX_PROPERTIES_NO_ERROR) { 368 break; 369 } else { 370 cxmutstr v = cx_strdup_a(allocator, value); 371 if (v.ptr == NULL) { 372 status = CX_PROPERTIES_MAP_ERROR; 373 break; 374 } 375 void *mv = use_cstring ? (void*)v.ptr : &v; 376 if (cxMapPut(target, key, mv)) { 377 cx_strfree(&v); 378 status = CX_PROPERTIES_MAP_ERROR; 379 break; 380 } 381 keys_found++; 382 } 383 } 384 if (status > CX_PROPERTIES_OK) { 385 break; 386 } 387 } 388 389 // cleanup and exit 390 fclose(f); 391 cxPropertiesDestroy(&parser); 392 cx_strfree(&fname); 393 if (status == CX_PROPERTIES_NO_DATA && keys_found > 0) { 394 return CX_PROPERTIES_NO_ERROR; 395 } else { 396 return status; 397 } 398 } 399