/* * db.c - package database * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Database format (plain text, records separated by blank lines): * \n * \n * \n relative paths, no leading / * \n * \n */ #include "db.h" #include #include #include #include #include #include #include #include #include static char *get_dbdir(const char *root) { const char *env = getenv("PKG_DBDIR"); /* if env is set and absolute, use it directly — don't join with root */ if (env && env[0] == '/') return xstrdup(env); return path_join(root, env && env[0] ? env : PKG_DBDIR); } pkg_t *pkg_new(const char *name, const char *version) { pkg_t *p = xmalloc(sizeof(pkg_t)); p->name = xstrdup(name); p->version = xstrdup(version); p->files = strarray_new(); p->next = NULL; return p; } void pkg_free(pkg_t *p) { if (!p) return; free(p->name); free(p->version); strarray_free(p->files); free(p); } pkgdb_t *db_open(const char *root, int exclusive) { pkgdb_t *db; char *dbfile; FILE *fp; char *line = NULL; size_t len = 0; ssize_t n; pkg_t *pkg = NULL; enum { DB_NAME, DB_VERSION, DB_FILES } state = DB_NAME; db = xmalloc(sizeof(pkgdb_t)); db->head = NULL; db->root = xstrdup(root ? root : "/"); db->dbdir = get_dbdir(db->root); db->fd = -1; if (mkdirp(db->dbdir, 0755) < 0) err(1, "cannot create %s", db->dbdir); db->fd = open(db->dbdir, O_RDONLY | O_DIRECTORY); if (db->fd < 0) err(1, "cannot open %s", db->dbdir); if (flock(db->fd, (exclusive ? LOCK_EX : LOCK_SH) | LOCK_NB) < 0) { if (errno == EWOULDBLOCK) errx(1, "package database is locked by another process"); err(1, "cannot lock %s", db->dbdir); } dbfile = path_join(db->dbdir, PKG_DBFILE); fp = fopen(dbfile, "r"); free(dbfile); if (!fp) { if (errno == ENOENT) return db; err(1, "cannot open database"); } while ((n = getline(&line, &len, fp)) != -1) { if (n > 0 && line[n - 1] == '\n') line[--n] = '\0'; if (n == 0) { if (pkg) { pkg->next = db->head; db->head = pkg; pkg = NULL; } state = DB_NAME; } else if (state == DB_NAME) { pkg = pkg_new(line, ""); state = DB_VERSION; } else if (state == DB_VERSION) { free(pkg->version); pkg->version = xstrdup(line); state = DB_FILES; } else { strarray_add(pkg->files, line); } } /* handle last record if file has no trailing newline */ if (pkg) { pkg->next = db->head; db->head = pkg; } if (ferror(fp)) err(1, "error reading database"); free(line); fclose(fp); return db; } void db_close(pkgdb_t *db) { pkg_t *p, *next; if (!db) return; for (p = db->head; p; p = next) { next = p->next; pkg_free(p); } if (db->fd >= 0) { flock(db->fd, LOCK_UN); close(db->fd); } free(db->root); free(db->dbdir); free(db); } int db_write(pkgdb_t *db) { char *dbfile, *tmpfile; FILE *fp; pkg_t *p; size_t i; int fd; dbfile = path_join(db->dbdir, PKG_DBFILE); tmpfile = path_join(db->dbdir, PKG_DBFILE ".tmp"); fd = open(tmpfile, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { warn("cannot create %s", tmpfile); free(dbfile); free(tmpfile); return -1; } fp = fdopen(fd, "w"); if (!fp) { close(fd); free(dbfile); free(tmpfile); return -1; } for (p = db->head; p; p = p->next) { if (p->files->count == 0) continue; fprintf(fp, "%s\n%s\n", p->name, p->version); strarray_sort(p->files); for (i = 0; i < p->files->count; i++) fprintf(fp, "%s\n", p->files->items[i]); fputc('\n', fp); } if (fflush(fp) != 0 || ferror(fp)) { warn("write error on %s", tmpfile); fclose(fp); unlink(tmpfile); free(dbfile); free(tmpfile); return -1; } fsync(fd); fclose(fp); if (rename(tmpfile, dbfile) < 0) { warn("cannot rename %s to %s", tmpfile, dbfile); unlink(tmpfile); free(dbfile); free(tmpfile); return -1; } /* fsync the directory to make the rename durable */ { int dfd = open(db->dbdir, O_RDONLY | O_DIRECTORY); if (dfd >= 0) { fsync(dfd); close(dfd); } } free(dbfile); free(tmpfile); return 0; } pkg_t *db_find(pkgdb_t *db, const char *name) { pkg_t *p; for (p = db->head; p; p = p->next) if (strcmp(p->name, name) == 0) return p; return NULL; } int db_add(pkgdb_t *db, pkg_t *p) { pkg_t *cur; /* * Guard against the caller passing a pkg pointer that's already * in the list. db_remove(by name) would free `p` itself, leaving * us linking freed memory. Today's callers always pass a fresh * pkg_new() result, but the check is cheap and defensive. */ for (cur = db->head; cur; cur = cur->next) { if (cur == p) return 0; /* already in list */ } db_remove(db, p->name); p->next = db->head; db->head = p; return 0; } int db_remove(pkgdb_t *db, const char *name) { pkg_t *p, *prev = NULL; for (p = db->head; p; prev = p, p = p->next) { if (strcmp(p->name, name) == 0) { if (prev) prev->next = p->next; else db->head = p->next; pkg_free(p); return 0; } } return -1; }