database.c
Go to the documentation of this file.
1 /* ========================================================================== */
2 /*! \file
3  * \brief Article header database (cache)
4  *
5  * Copyright (c) 2012-2021 by the developers. See the LICENSE file for details.
6  *
7  * If nothing else is specified, function return zero to indicate success
8  * and a negative value to indicate an error.
9  */
10 
11 
12 /* ========================================================================== */
13 /* Include headers */
14 
15 #include "posix.h" /* Include this first because of feature test macros */
16 
17 #include <string.h>
18 
19 #include "database.h"
20 #include "encoding.h"
21 #include "fileutils.h"
22 #include "main.h"
23 #include "xdg.h"
24 
25 
26 /* ========================================================================== */
27 /*! \defgroup DATABASE DATA: Database for header cache
28  *
29  * Location of article header database: \c $XDG_CONFIG_HOME/$CFG_NAME/headers
30  *
31  * This database use no special data structures, instead a subdirectory is
32  * created for every group. Inside this directory, for every entry a regular
33  * file that contains the article header is created with the article watermark
34  * as its name.
35  *
36  * Every new entry is first written to a temporary file \c .tmp , then pushed to
37  * disk and finally merged into the database by atomically renaming the
38  * temporary file.
39  *
40  * All this together makes this database slow and inefficient, but very robust.
41  * The data structures should never become damaged - even if the program crash
42  * while currently writing to the database it does not become corrupt and no
43  * special recovery is necessary to make it usable again.
44  */
45 /*! @{ */
46 
47 
48 /* ========================================================================== */
49 /* Constants */
50 
51 /*! \brief Message prefix for DATABASE module */
52 #define MAIN_ERR_PREFIX "DATA: "
53 
54 /*! \brief Permissions for database content files */
55 #define DB_PERM (posix_mode_t) (POSIX_S_IRUSR | POSIX_S_IWUSR)
56 
57 
58 /* ========================================================================== */
59 /* Variables */
60 
61 static posix_pthread_mutex_t db_mutex = POSIX_PTHREAD_MUTEX_INITIALIZER;
62 static int db_mutex_state = 0;
63 static const char* db_path = NULL;
64 static size_t db_path_len = 0;
65 
66 
67 /* ========================================================================== */
68 /* Lock mutex
69  *
70  * The current state of the mutex is stored in \ref db_mutex_state and nothing
71  * is done if \ref db_mutex is already locked.
72  *
73  * \attention
74  * Locking the \ref db_mutex and updating \ref db_mutex_state must be an atomic
75  * operation.
76  */
77 
78 static int db_mutex_lock()
79 {
80  int res = -1;
81  int rv;
82  int cs;
83 
84  if(!db_mutex_state)
85  {
86  rv = posix_pthread_setcancelstate(POSIX_PTHREAD_CANCEL_DISABLE, &cs);
87  if(rv) { PRINT_ERROR("Setting thread cancelability state failed"); }
88  else
89  {
90  rv = posix_pthread_mutex_lock(&db_mutex);
91  if(rv) { PRINT_ERROR("Locking mutex failed"); }
92  else { db_mutex_state = 1; res = 0; }
93  rv = posix_pthread_setcancelstate(cs, &cs);
94  if(rv)
95  {
96  PRINT_ERROR("Restoring thread cancelability state failed");
97  }
98  }
99  }
100 
101  return(res);
102 }
103 
104 
105 /* ========================================================================== */
106 /* Unlock mutex
107  *
108  * The current state of the mutex is stored in \ref db_mutex_state and nothing
109  * is done if \ref db_mutex is already unlocked.
110  *
111  * \attention
112  * Locking the \ref db_mutex and updating \ref db_mutex_state must be an atomic
113  * operation.
114  */
115 
116 static int db_mutex_unlock()
117 {
118  int res = -1;
119  int rv;
120  int cs;
121 
122  if(db_mutex_state)
123  {
124  rv = posix_pthread_setcancelstate(POSIX_PTHREAD_CANCEL_DISABLE, &cs);
125  if(rv) { PRINT_ERROR("Setting thread cancelability state failed"); }
126  else
127  {
128  rv = posix_pthread_mutex_unlock(&db_mutex);
129  if(rv) { PRINT_ERROR("Unlocking mutex failed"); }
130  else { db_mutex_state = 0; res = 0; }
131  rv = posix_pthread_setcancelstate(cs, &cs);
132  if(rv)
133  {
134  PRINT_ERROR("Restoring thread cancelability state failed");
135  }
136  }
137  }
138 
139  return(res);
140 }
141 
142 
143 /* ========================================================================== */
144 /* Dummy compare function for \c scandir()
145  *
146  * \return
147  * - Always 0 so that the sort order will be undefined.
148  */
149 
150 static int db_compar_dummy(const struct_posix_dirent** a,
151  const struct_posix_dirent** b)
152 {
153  return(0);
154 }
155 
156 
157 /* ========================================================================== */
158 /* Numerical compare function for \c scandir()
159  *
160  * \note
161  * Leading zeros and non-digit characters are not supported and the result is
162  * undefined in this case.
163  *
164  * \return
165  * - 0 for equality
166  * - 1 if a is greater
167  * - -1 if b is greater
168  */
169 
170 static int db_compar_num(const struct_posix_dirent** a,
171  const struct_posix_dirent** b)
172 {
173  int res = 0;
174  const char* name_a = (*a)->d_name;
175  const char* name_b = (*b)->d_name;
176  size_t i = 0;
177 
178  while(1)
179  {
180  if(!name_a[i])
181  {
182  if(name_b[i]) { res = -1; }
183  break;
184  }
185  if(!name_b[i])
186  {
187  if(name_a[i]) { res = 1; }
188  break;
189  }
190  if(!res)
191  {
192  if(name_a[i] != name_b[i])
193  {
194  if(name_a[i] < name_b[i]) { res = -1; } else { res = 1; }
195  }
196  }
197  ++i;
198  }
199 
200  /* printf("compar: a=%s, b=%s => res=%d\n", name_a, name_b, res); */
201 
202  return(res);
203 }
204 
205 
206 /* ========================================================================== */
207 /* Get config file pathname
208  *
209  * The caller is responsible to free the memory for the buffer on success.
210  */
211 
212 static int db_get_path(const char** dbpath)
213 {
214  static const char dbdir[] = "headers/";
215  const char* confdir = xdg_get_confdir(CFG_NAME);
216  int res = -1;
217  int rv;
218 
219  /* Init result so that 'free()' can be called in all cases */
220  *dbpath = NULL;
221 
222  if(NULL != confdir)
223  {
224  *dbpath = confdir;
225  rv = xdg_append_to_path(dbpath, dbdir);
226  if(0 == rv)
227  {
228  /* Create database directory if it doesn't exist */
229  res = fu_create_path(*dbpath, (posix_mode_t) POSIX_S_IRWXU);
230  }
231  }
232 
233  /* Free memory on error */
234  if(res)
235  {
236  PRINT_ERROR("Cannot create database directory");
237  posix_free((void*) *dbpath);
238  *dbpath = NULL;
239  }
240 
241  return(res);
242 }
243 
244 
245 /* ========================================================================== */
246 /* Init database without locking mutex */
247 
248 static int db_init_unlocked(void)
249 {
250  int res = 0;
251 
252  /* Return success if already initialized */
253  if(NULL == db_path)
254  {
255  res = db_get_path(&db_path);
256  if(!res) { db_path_len = strlen(db_path); }
257  else
258  {
259  PRINT_ERROR("Initializing database failed");
260  db_path = NULL;
261  db_path_len = 0;
262  }
263  }
264 
265  return(res);
266 }
267 
268 
269 /* ========================================================================== */
270 /* Shutdown database without locking mutex */
271 
272 static int db_exit_unlocked(void)
273 {
274  if(NULL != db_path)
275  {
276  posix_free((void*) db_path);
277  db_path = NULL;
278  db_path_len = 0;
279  }
280 
281  return(0);
282 }
283 
284 
285 /* ========================================================================== */
286 /*! \brief Init database
287  *
288  * \return
289  * - 0 on success
290  * - Negative value on error
291  */
292 
293 int db_init(void)
294 {
295  int res = -1;
296  int rv;
297 
298  rv = db_mutex_lock();
299  if(!rv)
300  {
301  res = db_init_unlocked();
302  db_mutex_unlock();
303  }
304 
305  return(res);
306 }
307 
308 
309 /* ========================================================================== */
310 /*! \brief Shutdown database
311  *
312  * \return
313  * - 0 on success
314  * - Negative value on error
315  */
316 
317 int db_exit(void)
318 {
319  int res = -1;
320  int rv;
321 
322  rv = db_mutex_lock();
323  if(!rv)
324  {
325  res = db_exit_unlocked();
326  db_mutex_unlock();
327  }
328 
329  return(res);
330 }
331 
332 
333 /* ========================================================================== */
334 /*! \brief Delete all database content
335  *
336  * \return
337  * - 0 on success
338  * - Negative value on error
339  */
340 
341 int db_clear(void)
342 {
343  int res = -1;
344  int rv;
345 
346  rv = db_mutex_lock();
347  if(!rv)
348  {
349  if(NULL == db_path) { PRINT_ERROR("Database not initialized"); }
350  else { res = fu_delete_tree(db_path); }
351  /* Reinitialize without unlocking mutex */
352  db_exit_unlocked();
353  db_init_unlocked();
354  db_mutex_unlock();
355  }
356 
357  return(res);
358 }
359 
360 
361 /* ========================================================================== */
362 /*! \brief Delete database content for all groups that are \b not specified
363  *
364  * \param[in] groupcount Number of group names in array \e grouplist
365  * \param[in] grouplist Array of group names
366  *
367  * If \e groupcount is zero, the database content for all groups is deleted.
368  * The parameter \e grouplist is ignored in this case and may be \c NULL .
369  *
370  * \return
371  * - 0 on success
372  * - Negative value on error
373  */
374 
375 int db_update_groups(size_t groupcount, const char** grouplist)
376 {
377  int res = -1;
378  int rv;
379  int num;
380  struct_posix_dirent** content;
381  const char* entry;
382  size_t i;
383  size_t ii;
384  int found;
385  char* path;
386 
387  rv = db_mutex_lock();
388  if(!rv)
389  {
390  if(NULL == db_path) { PRINT_ERROR("Database not initialized"); }
391  else
392  {
393  /* Get groups currently present in database */
394  num = posix_scandir(db_path, &content, NULL, db_compar_dummy);
395  if(0 <= num)
396  {
397  for(i = 0; i < (size_t) num; ++i)
398  {
399  entry = content[i]->d_name;
400  /* Ignore "." and ".." entries */
401  if(!strcmp(".", entry)) { continue; }
402  if(!strcmp("..", entry)) { continue; }
403  /* Check whether group must be preserved */
404  found = 0;
405  for(ii = 0; ii < groupcount; ++ii)
406  {
407  if(!strcmp(grouplist[ii], entry))
408  {
409  found = 1;
410  break;
411  }
412  }
413  if(!found)
414  {
415  /* Remove group from database */
416  path = (char*) posix_malloc(strlen(db_path) + strlen(entry)
417  + (size_t) 1);
418  if(NULL == path)
419  {
420  PRINT_ERROR("Cannot allocate memory for path");
421  break;
422  }
423  else
424  {
425  strcpy(path, db_path);
426  strcat(path, entry);
427  res = fu_delete_tree(path);
428  posix_free((void*) path);
429  if(res) { break; }
430  }
431  }
432  }
433  /* Free memory allocated by scandir() */
434  while(num--) { posix_free((void*) content[num]); }
435  posix_free((void*) content);
436  }
437  }
438  db_mutex_unlock();
439  }
440 
441  return(res);
442 }
443 
444 
445 /* ========================================================================== */
446 /*! \brief Add entry
447  *
448  * \param[in] group Newsgroup of article
449  * \param[in] anum Article ID
450  * \param[in] header Pointer to article header
451  * \param[in] len Length of article header
452  *
453  * \return
454  * - 0 on success
455  * - Negative value on error
456  */
457 
458 int db_add(const char* group, core_anum_t anum,
459  const char* header, size_t len)
460 {
461  static char tmpfile[] = ".tmp";
462  static size_t tmpfile_len = sizeof(tmpfile) - (size_t) 1;
463  char file[17];
464  size_t file_len;
465  int res = -1;
466  char* tmppathname = NULL;
467  char* pathname = NULL;
468  int rv;
469  int fd;
470 
471  if(NULL == db_path)
472  {
473  PRINT_ERROR("Database not initialized");
474  return(res);
475  }
476 
477  /* Verify parameters */
478  if(NULL == group || !anum || NULL == header)
479  {
480  PRINT_ERROR("db_add() called with invalid parameters");
481  return(res);
482  }
483 
484  rv = db_mutex_lock();
485  if(!rv)
486  {
487  /* Calculate memory requirements for pathnames */
488  rv = enc_convert_anum_to_ascii(file, &file_len, anum);
489  if(!rv)
490  {
491  /* The additional bytes are for '/' and the terminating NUL */
492  tmppathname = (char*) posix_malloc(db_path_len + strlen(group)
493  + tmpfile_len + (size_t) 2);
494  pathname = (char*) posix_malloc(db_path_len + strlen(group)
495  + file_len + (size_t) 2);
496  if (NULL == tmppathname || NULL == pathname)
497  {
498  PRINT_ERROR("Cannot allocate memory for database pathname");
499  }
500  else
501  {
502  /* Create group directory if it doesn't exist */
503  strcpy(tmppathname, db_path);
504  strcat(tmppathname, group);
505  strcat(tmppathname, "/");
506  rv = posix_mkdir(tmppathname, (posix_mode_t) POSIX_S_IRWXU);
507  if (!rv || (-1 == rv && POSIX_EEXIST == posix_errno))
508  {
509  strcat(tmppathname, tmpfile);
510  /* Open and lock temporary file */
511  rv = fu_open_file(tmppathname, &fd,
512  POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
513  DB_PERM);
514  if(!rv)
515  {
516  rv = fu_lock_file(fd);
517  if(!rv)
518  {
519  /* Write header into temporary file */
520  rv = fu_write_to_filedesc(fd, header, len);
521  if(!rv)
522  {
523  rv = fu_sync(fd, NULL);
524  if(!rv)
525  {
526  /* Rename temporary file to new entry file */
527  strcpy(pathname, db_path);
528  strcat(pathname, group);
529  strcat(pathname, "/");
530  strcat(pathname, file);
531  rv = posix_rename(tmppathname, pathname);
532  }
533  }
534  if(rv)
535  {
536  /* Unlink temporary file on error */
537  PRINT_ERROR("Failed to store data");
538  if(NULL != tmppathname)
539  {
540  (void) fu_unlink_file(tmppathname);
541  }
542  }
543  else { res = 0; }
544  }
545  fu_close_file(&fd, NULL);
546  }
547  }
548  else { PRINT_ERROR("Cannot create group directory"); }
549  }
550  }
551  posix_free((void*) pathname);
552  posix_free((void*) tmppathname);
553  db_mutex_unlock();
554  }
555 
556  return(res);
557 }
558 
559 
560 /* ========================================================================== */
561 /*! \brief Read entry
562  *
563  * \param[in] group Newsgroup of article
564  * \param[in] anum Article ID
565  * \param[out] header Pointer to article header buffer
566  * \param[out] len Pointer to length of article header buffer (not content!)
567  *
568  * On success, the caller is responsible to free the memory allocated for the
569  * article header buffer.
570  *
571  * \return
572  * - 0 on success
573  * - Negative value on error
574  */
575 
576 int db_read(const char* group, core_anum_t anum, char** header,
577  size_t* len)
578 {
579  int res = -1;
580  int rv;
581  char file[17];
582  size_t file_len;
583  char* pathname = NULL;
584  int fd;
585 
586  if(NULL == db_path)
587  {
588  PRINT_ERROR("Database not initialized");
589  return(res);
590  }
591 
592  /* Verify parameters */
593  if(NULL == group || !anum || NULL == header)
594  {
595  PRINT_ERROR("db_read() called with invalid parameters");
596  return(res);
597  }
598 
599  rv = db_mutex_lock();
600  if(!rv)
601  {
602  /* Calculate memory requirements for pathname */
603  rv = enc_convert_anum_to_ascii(file, &file_len, anum);
604  if(!rv)
605  {
606  /* The additional bytes are for '/' and the terminating NUL */
607  pathname = (char*) posix_malloc(db_path_len + strlen(group)
608  + file_len + (size_t) 2);
609  if (NULL == pathname)
610  {
611  PRINT_ERROR("Cannot allocate memory for database pathname");
612  }
613  else
614  {
615  strcpy(pathname, db_path);
616  strcat(pathname, group);
617  strcat(pathname, "/");
618  strcat(pathname, file);
619  rv = fu_open_file(pathname, &fd, POSIX_O_RDWR, 0);
620  if(-1 != rv)
621  {
622  rv = fu_lock_file(fd);
623  if(!rv)
624  {
625  rv = fu_read_whole_file(fd, header, len);
626  if(!rv) { res = 0; }
627  }
628  fu_close_file(&fd, NULL);
629  }
630  }
631  }
632  posix_free((void*) pathname);
633  db_mutex_unlock();
634  }
635 
636  return(res);
637 }
638 
639 
640 /* ========================================================================== */
641 /*! \brief Delete entries
642  *
643  * \param[in] group Newsgroup of article
644  * \param[in] start Start ID of article range
645  * \param[in] end End ID of article range
646  *
647  * To delete all entries of \e group , specify both \e start and \e end as 0.
648  *
649  * To delete anything from the beginning up to \e end, specify 1 for \e start
650  * and this function determines the first entry automatically without trying
651  * to delete (in worst case) billions of nonexistent entries one by one.
652  *
653  * \return
654  * - 0 on success
655  * - Negative value on error
656  */
657 
658 int db_delete(const char* group, core_anum_t start, core_anum_t end)
659 {
660  int res = -1;
661  int rv;
662  char file[17];
663  size_t file_len;
664  char* pathname = NULL;
665  size_t path_len;
666  core_anum_t i = start;
667  struct_posix_dirent** content;
668  int num;
669  const char* entry;
670  size_t ii;
671  core_anum_t e;
672 
673  if(NULL == db_path)
674  {
675  PRINT_ERROR("Database not initialized");
676  return(res);
677  }
678 
679  /* Verify parameters */
680  if(NULL == group || end < start)
681  {
682  PRINT_ERROR("db_delete() called with invalid parameters");
683  return(res);
684  }
685 
686  rv = db_mutex_lock();
687  if(!rv)
688  {
689  /* Calculate maximum memory requirements for pathname */
690  /* The additional 2 bytes are for '/' and the terminating NUL */
691  pathname = (char*) posix_malloc(db_path_len + strlen(group)
692  + (size_t) 17 + (size_t) 2);
693  if (NULL == pathname)
694  {
695  PRINT_ERROR("Cannot allocate memory for database pathname");
696  }
697  else
698  {
699  strcpy(pathname, db_path);
700  strcat(pathname, group);
701  strcat(pathname, "/");
702  path_len = strlen(pathname);
703  /* Check whether whole group should be cleared */
704  if(!start && !end)
705  {
706  /* Yes => Delete subdirectory of group */
707  /* printf("Delete database subtree: %s\n", pathname); */
708  res = fu_delete_tree(pathname);
709  }
710  else if(!start || !end)
711  {
712  PRINT_ERROR("Invalid range specified for deletion");
713  }
714  else
715  {
716  /* Determine first entry of database */
717  num = posix_scandir(pathname, &content, NULL, db_compar_num);
718  if(0 <= num)
719  {
720  /* The entries were numerically sorted by 'scandir()' */
721  for(ii = 0; ii < (size_t) num; ++ii)
722  {
723  entry = content[ii]->d_name;
724  /* Ignore "." and ".." entries */
725  if(!strcmp(".", entry)) { continue; }
726  if(!strcmp("..", entry)) { continue; }
727  rv = enc_convert_ascii_to_anum(&e, entry,
728  (int) strlen(entry));
729  if(!rv)
730  {
731  /* Clamp range start to beginning of database content */
732  if(e > start) { i = e; }
733  }
734  break;
735  }
736  while(i <= end)
737  {
738  rv = enc_convert_anum_to_ascii(file, &file_len, i);
739  if(rv) { break; }
740  else
741  {
742  pathname[path_len] = 0;
743  strncpy(&pathname[path_len], file, 17);
744  /* Unlink file of entry */
745  (void) fu_unlink_file(pathname);
746  /* Continue if entry was not found */
747  }
748  if(i == end) { res = 0; }
749  ++i;
750  }
751  /* Free memory allocated by scandir() */
752  while(num--) { posix_free((void*) content[num]); }
753  posix_free((void*) content);
754  }
755  }
756  }
757  posix_free((void*) pathname);
758  db_mutex_unlock();
759  }
760 
761  return(res);
762 }
763 
764 
765 /*! @} */
766 
767 /* EOF */
fu_write_to_filedesc
int fu_write_to_filedesc(int filedesc, const char *buffer, size_t len)
Write data block to filedescriptor.
Definition: fileutils.c:542
core_anum_t
#define core_anum_t
Article number data type (value zero is always reserved)
Definition: core.h:24
db_exit
int db_exit(void)
Shutdown database.
Definition: database.c:317
fu_lock_file
int fu_lock_file(int filedesc)
Lock file for writing.
Definition: fileutils.c:328
enc_convert_ascii_to_anum
int enc_convert_ascii_to_anum(core_anum_t *result, const char *wm, int len)
Convert number from ASCII to numerical format.
Definition: encoding.c:4604
fu_create_path
int fu_create_path(const char *path, posix_mode_t perm)
Create path.
Definition: fileutils.c:119
db_update_groups
int db_update_groups(size_t groupcount, const char **grouplist)
Delete database content for all groups that are not specified.
Definition: database.c:375
fu_unlink_file
int fu_unlink_file(const char *pathname)
Unlink file.
Definition: fileutils.c:355
enc_convert_anum_to_ascii
int enc_convert_anum_to_ascii(char result[17], size_t *len, core_anum_t wm)
Convert article number from numerical format to ASCII.
Definition: encoding.c:4558
PRINT_ERROR
#define PRINT_ERROR(s)
Prepend module prefix and print error message.
Definition: main.h:19
db_init
int db_init(void)
Init database.
Definition: database.c:293
xdg_get_confdir
const char * xdg_get_confdir(const char *)
Get configuration directory.
Definition: xdg.c:115
db_read
int db_read(const char *group, core_anum_t anum, char **header, size_t *len)
Read entry.
Definition: database.c:576
db_add
int db_add(const char *group, core_anum_t anum, const char *header, size_t len)
Add entry.
Definition: database.c:458
xdg_append_to_path
int xdg_append_to_path(const char **, const char *)
Append path component to buffer.
Definition: xdg.c:55
fu_delete_tree
int fu_delete_tree(const char *dir)
Delete directory tree.
Definition: fileutils.c:578
db_delete
int db_delete(const char *group, core_anum_t start, core_anum_t end)
Delete entries.
Definition: database.c:658
fu_sync
int fu_sync(int filedesc, FILE *stream)
Flush buffers of file.
Definition: fileutils.c:402
fu_close_file
void fu_close_file(int *filedesc, FILE **stream)
Close file (and potentially associated I/O stream)
Definition: fileutils.c:290
DB_PERM
#define DB_PERM
Permissions for database content files.
Definition: database.c:55
db_clear
int db_clear(void)
Delete all database content.
Definition: database.c:341
fu_read_whole_file
int fu_read_whole_file(int filedesc, char **buffer, size_t *len)
Read text file content and store it into memory buffer.
Definition: fileutils.c:445
fu_open_file
int fu_open_file(const char *pathname, int *filedesc, int mode, posix_mode_t perm)
Open file.
Definition: fileutils.c:243

Generated at 2024-04-27 using  doxygen