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

Generated at 2026-01-27 using  doxygen