aboutsummaryrefslogtreecommitdiffstats
path: root/store/store-log.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 10:43:45 +0100
committereunhhu <[email protected]>2026-09-28 10:43:45 +0100
commitcff25dd387376311b09bab3a58377fecbfe818e4 (patch)
tree13dcd7c742785f3f13d2466e02e10012479d0418 /store/store-log.c
parent923576863660d995476dffc7b285ee20e9707f27 (diff)
downloadpure-cff25dd387376311b09bab3a58377fecbfe818e4.tar.gz
pure-cff25dd387376311b09bab3a58377fecbfe818e4.zip
Log an app's saves in reused segments and write list images whole beside the store
Diffstat (limited to 'store/store-log.c')
-rw-r--r--store/store-log.c572
1 files changed, 572 insertions, 0 deletions
diff --git a/store/store-log.c b/store/store-log.c
new file mode 100644
index 0000000..6138a84
--- /dev/null
+++ b/store/store-log.c
@@ -0,0 +1,572 @@
+/* The app log and list images (core/log.h).
+ *
+ * The log's entries go into segments, files log-0, log-1, ... in the store's
+ * directory, each allocated whole when made, so that an append neither grows
+ * a file nor takes new blocks and its sync writes the entry alone, and used
+ * again once every entry in it is released. A segment begins with a header
+ * naming its generation (higher than any before it) and the generation of the
+ * segment it continues; an entry is its save's number, the number and digest
+ * of the entry before it (0 for the first), its payload's length and flags,
+ * a digest of those and the payload, then the payload, padded to 8 bytes.
+ * Opening reads each segment's entries while each names the one before it,
+ * and chains the segments from the newest back by the generations and entries
+ * they name: a torn entry, or bytes a segment kept from an earlier use, end
+ * what is read. Save numbers may skip (a save that changed no list writes no
+ * entry) and are used again (for an entry taken back), so an entry is known
+ * by the digest of the one it names, not by numbers alone.
+ *
+ * An image is written to a temporary file in 256 KiB steps, each started to
+ * the device as the next is written, then synced and renamed over the image
+ * it replaces: a writer never has more than two steps waiting, so the log's
+ * syncs between them wait for little. */
+#if defined(__linux__)
+#define _GNU_SOURCE
+#else
+#define _POSIX_C_SOURCE 200809L
+#endif
+#ifdef __APPLE__
+#define _DARWIN_C_SOURCE
+#endif
+#include "log.h"
+#include "store.h"
+#include "digest.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+enum { HEADER = 64, ENTRY = 40, SEGMENTS = 256, STEP = 256 << 10, TREE = 1 };
+#ifdef PURE_STORE_TEST
+enum { SEGMENT = 16384 };
+#else
+enum { SEGMENT = 8 << 20 };
+#endif
+static const unsigned char log_magic[8] = {'P', 'U', 'R', 'E', 'L', 'O', 'G', '1'};
+static const unsigned char image_magic[8] = {'P', 'U', 'R', 'E', 'I', 'M', 'G', '1'};
+
+static bool fault(PureStoreStage stage) {
+#ifdef PURE_STORE_TEST
+ if (pure_store_test_fault(stage)) { errno = EIO; return true; }
+#else
+ (void)stage;
+#endif
+ return false;
+}
+
+static uint64_t get64(const unsigned char *bytes) {
+ uint64_t value = 0;
+ for (unsigned i = 0; i < 8; ++i) value |= (uint64_t)bytes[i] << (8 * i);
+ return value;
+}
+
+static void put64(unsigned char *bytes, uint64_t value) {
+ for (unsigned i = 0; i < 8; ++i) bytes[i] = (unsigned char)(value >> (8 * i));
+}
+
+static bool write_all(int fd, const unsigned char *bytes, size_t length, uint64_t offset) {
+ while (length) {
+ ssize_t n = pwrite(fd, bytes, length, (off_t)offset);
+ if (n < 0 && errno == EINTR) continue;
+ if (n <= 0) return false;
+ bytes += (size_t)n;
+ length -= (size_t)n;
+ offset += (uint64_t)n;
+ }
+ return true;
+}
+
+static bool sync_whole(int fd) {
+ int rc;
+ do { rc = fsync(fd); } while (rc && errno == EINTR);
+ return rc == 0;
+}
+
+static bool sync_data(int fd) {
+#ifdef __APPLE__
+ return sync_whole(fd);
+#else
+ int rc;
+ do { rc = fdatasync(fd); } while (rc && errno == EINTR);
+ return rc == 0;
+#endif
+}
+
+/* Room for size bytes from the start of a file, allocated. */
+static bool allocate(int fd, uint64_t size) {
+#if defined(__APPLE__)
+ return !ftruncate(fd, (off_t)size);
+#else
+ int rc = posix_fallocate(fd, 0, (off_t)size);
+ return rc == 0 || ((rc == EOPNOTSUPP || rc == EINVAL) && !ftruncate(fd, (off_t)size));
+#endif
+}
+
+/* ---- The log */
+
+typedef struct {
+ int fd;
+ uint64_t generation; /* 0: no header */
+ uint64_t predecessor; /* the generation of the segment it continues (0: none) */
+ uint64_t size; /* bytes allocated */
+ /* Its entries (first 0: none), the entry named before the first (and its
+ * digest), the last that counts (and its digest) and where that one ends. */
+ uint64_t first, previous, named, last, digest, end;
+} Segment;
+
+struct PureLog {
+ int directory;
+ Segment segment[SEGMENTS];
+ unsigned count;
+ int current; /* the segment appended to, or -1 */
+ uint64_t last, digest; /* the last entry's number and digest */
+ uint64_t generation; /* the highest any header has had */
+ uint64_t released;
+ /* The segments as opened, mapped until the first append (a start reads
+ * them; what is appended after is never read in the same run). */
+ unsigned char *maps[SEGMENTS];
+ size_t mapped[SEGMENTS];
+ /* The last append, to take back: its segment as it was and the last entry before it. */
+ int appended;
+ Segment before;
+ uint64_t last_before, digest_before;
+ unsigned char *buffer;
+ size_t capacity;
+};
+
+static uint64_t entry_digest(const unsigned char *head, const unsigned char *payload, size_t size) {
+ Digest digest = {0};
+ digest_add(&digest, head, ENTRY - 8);
+ digest_add(&digest, payload, size);
+ return digest_end(&digest);
+}
+
+static size_t padded(size_t size) { return (size + 7) & ~(size_t)7; }
+static uint32_t entry_length(const unsigned char *head) { return (uint32_t)get64(head + 16); }
+
+/* The entries of a mapped segment from its header on, each naming the one
+ * before: the first (and the entry it names), the last, and their end. */
+static void scan(Segment *s, const unsigned char *map, size_t size) {
+ s->first = s->previous = s->named = s->last = s->digest = 0;
+ s->end = HEADER;
+ uint64_t at = HEADER;
+ while (at + ENTRY <= size) {
+ const unsigned char *head = map + at;
+ uint64_t seq = get64(head), previous = get64(head + 8), named = get64(head + 24);
+ uint32_t length = entry_length(head);
+ if (!seq || (s->first && (previous != s->last || named != s->digest || seq <= s->last)) ||
+ (!s->first && previous >= seq) || length > size - at - ENTRY ||
+ get64(head + 32) != entry_digest(head, head + ENTRY, length))
+ break;
+ if (!s->first) s->first = seq, s->previous = previous, s->named = named;
+ s->last = seq;
+ s->digest = get64(head + 32);
+ at += ENTRY + padded(length);
+ s->end = at;
+ }
+}
+
+/* Where entry seq of a segment as opened ends (0: not among its entries),
+ * and its digest. */
+static uint64_t end_of(const PureLog *log, unsigned i, uint64_t seq, uint64_t *digest) {
+ const Segment *s = &log->segment[i];
+ const unsigned char *map = log->maps[i];
+ if (!map || !s->first || seq < s->first || seq > s->last) return 0;
+ for (uint64_t at = HEADER; at < s->end;) {
+ const unsigned char *head = map + at;
+ at += ENTRY + padded(entry_length(head));
+ if (get64(head) == seq) {
+ *digest = get64(head + 32);
+ return at;
+ }
+ }
+ return 0;
+}
+
+static int generation_at(const PureLog *log, uint64_t generation) {
+ for (unsigned i = 0; generation && i < log->count; ++i)
+ if (log->segment[i].generation == generation) return (int)i;
+ return -1;
+}
+
+/* Zeros over an entry's header: what follows it is no longer read. */
+static bool erase(const Segment *s, uint64_t at) {
+ static const unsigned char zeros[ENTRY];
+ return at + ENTRY > s->size || (write_all(s->fd, zeros, ENTRY, at) && sync_data(s->fd));
+}
+
+static void unmap(PureLog *log) {
+ for (unsigned i = 0; i < log->count; ++i)
+ if (log->maps[i]) {
+ munmap(log->maps[i], log->mapped[i]);
+ log->maps[i] = NULL;
+ }
+}
+
+PureResult pure_log_open(const char *directory, PureLog **out) {
+ *out = NULL;
+ PureLog *log = calloc(1, sizeof(*log));
+ if (!log) return PURE_ALLOCATION;
+ log->current = log->appended = -1;
+ log->directory = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (log->directory < 0) { free(log); return PURE_STORE_ERROR; }
+ PureResult result = PURE_OK;
+ for (unsigned n = 0; n < SEGMENTS; ++n) {
+ char name[24];
+ snprintf(name, sizeof(name), "log-%u", n);
+ int fd = openat(log->directory, name, O_RDWR | O_NOFOLLOW | O_CLOEXEC);
+ if (fd < 0) {
+ if (errno != ENOENT) result = PURE_STORE_ERROR;
+ break;
+ }
+ struct stat file;
+ Segment *s = &log->segment[log->count++];
+ *s = (Segment){.fd = fd, .end = HEADER};
+ if (fstat(fd, &file)) { result = PURE_STORE_ERROR; break; }
+ s->size = (uint64_t)file.st_size;
+ if (s->size < HEADER) continue;
+ void *map = mmap(NULL, (size_t)s->size, PROT_READ, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED) { result = PURE_STORE_ERROR; break; }
+ log->maps[log->count - 1] = map;
+ log->mapped[log->count - 1] = (size_t)s->size;
+ const unsigned char *header = map;
+ if (memcmp(header, log_magic, 8) || get64(header + HEADER - 8) != digest_of(header, HEADER - 8)) continue;
+ s->generation = get64(header + 8);
+ s->predecessor = get64(header + 16);
+ if (s->generation > log->generation) log->generation = s->generation;
+ scan(s, map, (size_t)s->size);
+ }
+ if (result != PURE_OK) { pure_log_close(log); return result; }
+ /* The newest segment with entries holds the last; from it back, each
+ * segment names the one it continues, which counts up to the entry the
+ * later one names first. The rest hold nothing the log still has. */
+ int top = -1;
+ for (unsigned i = 0; i < log->count; ++i)
+ if (log->segment[i].first && (top < 0 || log->segment[i].generation > log->segment[top].generation)) top = (int)i;
+ bool chained[SEGMENTS] = {false};
+ for (int at = top, later = -1; at >= 0; later = at, at = generation_at(log, log->segment[at].predecessor)) {
+ Segment *s = &log->segment[at];
+ if (chained[at] || s->generation >= (later >= 0 ? log->segment[later].generation : UINT64_MAX)) break;
+ if (later >= 0) { /* it counts up to the entry the later segment names */
+ uint64_t digest, ends = end_of(log, (unsigned)at, log->segment[later].previous, &digest);
+ if (!ends || digest != log->segment[later].named) break; /* released: known from the later segment on */
+ s->last = log->segment[later].previous;
+ s->digest = digest;
+ s->end = ends;
+ }
+ chained[at] = true;
+ if (!s->previous) break;
+ }
+ for (unsigned i = 0; i < log->count; ++i)
+ if (!chained[i]) {
+ Segment *s = &log->segment[i];
+ s->first = s->previous = s->last = 0;
+ s->end = HEADER;
+ }
+ if (top >= 0) {
+ log->current = top;
+ log->last = log->segment[top].last;
+ log->digest = log->segment[top].digest;
+ }
+ *out = log;
+ return PURE_OK;
+}
+
+void pure_log_close(PureLog *log) {
+ if (!log) return;
+ unmap(log);
+ for (unsigned i = 0; i < log->count; ++i) close(log->segment[i].fd);
+ close(log->directory);
+ free(log->buffer);
+ free(log);
+}
+
+uint64_t pure_log_last(const PureLog *log) { return log->last; }
+
+uint64_t pure_log_bytes(const PureLog *log) {
+ uint64_t bytes = 0;
+ for (unsigned i = 0; i < log->count; ++i) {
+ const Segment *s = &log->segment[i];
+ if (s->first && ((int)i == log->current || s->last > log->released)) bytes += s->end;
+ }
+ return bytes;
+}
+
+PureResult pure_log_read(PureLog *log, uint64_t from,
+ bool (*visit)(void *, uint64_t seq, bool tree, PureSpan payload), void *context) {
+ if (log->current < 0 || log->last < from) return PURE_OK;
+ int chain[SEGMENTS];
+ unsigned links = 0;
+ for (int at = log->current; at >= 0 && links < SEGMENTS;) {
+ const Segment *s = &log->segment[at];
+ if (!s->first) break;
+ chain[links++] = at;
+ if (s->first <= from || !s->previous) break;
+ at = generation_at(log, s->predecessor);
+ }
+ if (!links) return PURE_OK;
+ const Segment *oldest = &log->segment[chain[links - 1]];
+ if (from && oldest->first > from && oldest->previous >= from) return PURE_STORE_ERROR; /* entries from `from` on are gone */
+ while (links--) {
+ const unsigned char *map = log->maps[chain[links]];
+ const Segment *s = &log->segment[chain[links]];
+ for (uint64_t at = HEADER; map && at < s->end;) {
+ const unsigned char *head = map + at;
+ uint64_t seq = get64(head);
+ if (seq >= from && !visit(context, seq, (get64(head + 16) >> 32) & TREE, (PureSpan){head + ENTRY, entry_length(head)}))
+ return PURE_OK;
+ at += ENTRY + padded(entry_length(head));
+ }
+ }
+ return PURE_OK;
+}
+
+void pure_log_end(PureLog *log, uint64_t seq) {
+ if (seq >= log->last) return;
+ /* The chained segment holding seq keeps its entries up to it; after it,
+ * there and in every newer segment, the device is erased too, so that
+ * opening again finds none of them. */
+ int holder = -1;
+ for (int at = log->current; at >= 0; at = generation_at(log, log->segment[at].predecessor)) {
+ const Segment *s = &log->segment[at];
+ if (!s->first) break;
+ if (s->first <= seq && seq <= s->last) { holder = at; break; }
+ if (!s->previous) break;
+ }
+ uint64_t kept = holder >= 0 ? log->segment[holder].generation : 0, digest = 0;
+ for (unsigned i = 0; i < log->count; ++i) {
+ Segment *s = &log->segment[i];
+ if ((int)i == holder) {
+ s->end = end_of(log, i, seq, &digest);
+ s->last = seq;
+ s->digest = digest;
+ erase(s, s->end);
+ } else if (s->generation > kept) {
+ erase(s, HEADER);
+ s->first = s->previous = s->last = 0;
+ s->end = HEADER;
+ }
+ }
+ log->current = holder;
+ log->last = seq;
+ log->digest = digest;
+}
+
+/* A segment to begin: one holding no entry still counted, else a new file. */
+static int fresh_segment(PureLog *log, uint64_t needed) {
+ int pick = -1;
+ for (unsigned i = 0; i < log->count; ++i) {
+ const Segment *s = &log->segment[i];
+ if ((int)i == log->current || (s->first && s->last > log->released)) continue;
+ if (pick < 0 || s->size > log->segment[pick].size) pick = (int)i;
+ }
+ if (pick >= 0) {
+ Segment *s = &log->segment[pick];
+ if (s->size < needed && !allocate(s->fd, needed)) return -1;
+ if (s->size < needed) s->size = needed;
+ return pick;
+ }
+ if (log->count == SEGMENTS) return -1;
+ char name[24];
+ snprintf(name, sizeof(name), "log-%u", log->count);
+ int fd = openat(log->directory, name, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
+ uint64_t size = needed > SEGMENT ? needed : SEGMENT;
+ /* The new file and its name are durable before an entry in it counts. */
+ if (fd < 0 || !allocate(fd, size) || !sync_whole(fd) || !sync_whole(log->directory)) {
+ if (fd >= 0) {
+ close(fd);
+ unlinkat(log->directory, name, 0);
+ }
+ return -1;
+ }
+ log->segment[log->count] = (Segment){.fd = fd, .size = size, .end = HEADER};
+ return (int)log->count++;
+}
+
+PureResult pure_log_append(PureLog *log, uint64_t seq, bool tree, PureSpan payload) {
+ if (seq <= log->last || payload.size > UINT32_MAX) return PURE_INVALID;
+ unmap(log);
+ size_t entry = ENTRY + padded(payload.size);
+ int at = log->current;
+ Segment *s = at >= 0 ? &log->segment[at] : NULL;
+ /* A segment with no entry yet (a first one taken back) takes the next
+ * entry after its header, grown to hold it. */
+ if (s && !s->first && s->end + entry > s->size) {
+ if (!allocate(s->fd, s->end + entry)) return PURE_STORE_ERROR;
+ s->size = s->end + entry;
+ }
+ bool begin = !s || s->end + entry > s->size;
+ if (begin && (at = fresh_segment(log, HEADER + entry)) < 0) return PURE_STORE_ERROR;
+ s = &log->segment[at];
+ size_t size = (begin ? HEADER : 0) + entry;
+ if (log->capacity < size) {
+ size_t capacity = log->capacity ? log->capacity : 65536;
+ while (capacity < size) capacity *= 2;
+ unsigned char *grown = realloc(log->buffer, capacity);
+ if (!grown) return PURE_ALLOCATION;
+ log->buffer = grown;
+ log->capacity = capacity;
+ }
+ uint64_t generation = begin ? log->generation + 1 : s->generation, offset = begin ? 0 : s->end;
+ uint64_t predecessor = begin ? (log->current >= 0 ? log->segment[log->current].generation : 0) : s->predecessor;
+ unsigned char *out = log->buffer;
+ if (begin) { /* a segment's header goes with its first entry, in one write */
+ memset(out, 0, HEADER);
+ memcpy(out, log_magic, 8);
+ put64(out + 8, generation);
+ put64(out + 16, predecessor);
+ put64(out + HEADER - 8, digest_of(out, HEADER - 8));
+ out += HEADER;
+ }
+ put64(out, seq);
+ put64(out + 8, log->last);
+ put64(out + 16, (uint64_t)payload.size | (uint64_t)(tree ? TREE : 0) << 32);
+ put64(out + 24, log->digest);
+ if (payload.size) memcpy(out + ENTRY, payload.data, payload.size);
+ memset(out + ENTRY + payload.size, 0, padded(payload.size) - payload.size);
+ uint64_t digest = entry_digest(out, out + ENTRY, payload.size);
+ put64(out + 32, digest);
+ if (fault(PURE_LOG_BEFORE_WRITE)) return PURE_STORE_ERROR; /* nothing written: no entry */
+ if (!write_all(s->fd, log->buffer, size, offset) || fault(PURE_LOG_BEFORE_SYNC) || !sync_data(s->fd))
+ return PURE_UNCERTAIN;
+ if (begin) {
+ log->generation = generation;
+ *s = (Segment){.fd = s->fd, .size = s->size, .generation = generation, .predecessor = predecessor, .end = HEADER};
+ }
+ log->before = *s;
+ log->last_before = log->last;
+ log->digest_before = log->digest;
+ log->appended = at;
+ if (!s->first) s->first = seq, s->previous = log->last, s->named = log->digest;
+ s->last = seq;
+ s->digest = digest;
+ s->end = offset + (begin ? HEADER : 0) + entry;
+ log->last = seq;
+ log->digest = digest;
+ log->current = at;
+ return PURE_OK;
+}
+
+void pure_log_retract(PureLog *log) {
+ if (log->appended < 0) return;
+ /* The segment keeps its header even when the entry was its first: it
+ * stays the one appended to, and the next entry takes this one's place. */
+ log->segment[log->appended] = log->before;
+ log->current = log->appended;
+ log->last = log->last_before;
+ log->digest = log->digest_before;
+ log->appended = -1;
+}
+
+void pure_log_release(PureLog *log, uint64_t seq) {
+ if (seq > log->released) log->released = seq;
+}
+
+/* ---- Images */
+
+static bool image_name(char out[64], const char *name, const char *prefix) {
+ size_t length = strlen(name);
+ if (!length || length > 40) return false;
+ for (size_t i = 0; i < length; ++i) {
+ char c = name[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return false;
+ }
+ snprintf(out, 64, "%simage-%s", prefix, name);
+ return true;
+}
+
+PureResult pure_image_write(const char *directory, const char *name, const PureSpan *parts, size_t count) {
+ char final[64], temporary[96];
+ if (!image_name(final, name, "")) return PURE_INVALID;
+ static uint64_t sequence;
+ snprintf(temporary, sizeof(temporary), ".%s-%ld-%llu", final, (long)getpid(),
+ (unsigned long long)__atomic_add_fetch(&sequence, 1, __ATOMIC_RELAXED));
+ int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (dir < 0) return PURE_STORE_ERROR;
+ int fd = openat(dir, temporary, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
+ if (fd < 0) { close(dir); return PURE_STORE_ERROR; }
+ uint64_t length = 0;
+ for (size_t i = 0; i < count; ++i) length += parts[i].size;
+ unsigned char head[16];
+ memcpy(head, image_magic, 8);
+ put64(head + 8, length);
+ Digest digest = {0};
+ bool ok = write_all(fd, head, sizeof(head), 0);
+ uint64_t at = sizeof(head), started = 0;
+ for (size_t i = 0; ok && i < count; ++i)
+ for (size_t done = 0; ok && done < parts[i].size;) {
+ size_t take = parts[i].size - done < STEP ? parts[i].size - done : STEP;
+ digest_add(&digest, parts[i].data + done, take);
+ ok = write_all(fd, parts[i].data + done, take, at);
+ done += take;
+ at += take;
+#if defined(__linux__)
+ /* Written steps go to the device as the next are written; a
+ * writer waits for the step before the last. */
+ if (ok && at - started >= STEP) {
+ sync_file_range(fd, (off_t)started, (off_t)(at - started), SYNC_FILE_RANGE_WRITE);
+ if (started >= STEP)
+ sync_file_range(fd, (off_t)(started - STEP), STEP,
+ SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER);
+ started = at;
+ }
+#endif
+ }
+ unsigned char tail[8];
+ put64(tail, digest_end(&digest));
+ ok = ok && write_all(fd, tail, sizeof(tail), at) && !fault(PURE_IMAGE_BEFORE_RENAME) && sync_data(fd);
+ if (close(fd)) ok = false;
+ ok = ok && !renameat(dir, temporary, dir, final);
+ if (!ok) unlinkat(dir, temporary, 0);
+ ok = ok && sync_whole(dir);
+ close(dir);
+ return ok ? PURE_OK : PURE_STORE_ERROR;
+}
+
+PureResult pure_image_read(const char *directory, const char *name, unsigned char **bytes, size_t *size) {
+ char final[64];
+ *bytes = NULL;
+ *size = 0;
+ if (!image_name(final, name, "")) return PURE_INVALID;
+ int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (dir < 0) return PURE_STORE_ERROR;
+ int fd = openat(dir, final, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+ close(dir);
+ if (fd < 0) return errno == ENOENT ? PURE_NOT_FOUND : PURE_STORE_ERROR;
+ struct stat file;
+ unsigned char *all = NULL;
+ PureResult result = PURE_STORE_ERROR;
+ if (!fstat(fd, &file) && file.st_size >= 24 && (all = malloc((size_t)file.st_size))) {
+ size_t have = 0, want = (size_t)file.st_size;
+ while (have < want) {
+ ssize_t n = pread(fd, all + have, want - have, (off_t)have);
+ if (n < 0 && errno == EINTR) continue;
+ if (n <= 0) break;
+ have += (size_t)n;
+ }
+ uint64_t length = have == want ? get64(all + 8) : UINT64_MAX;
+ if (have == want && !memcmp(all, image_magic, 8) && length == want - 24 &&
+ get64(all + 16 + length) == digest_of(all + 16, (size_t)length)) {
+ memmove(all, all + 16, (size_t)length);
+ *bytes = all;
+ *size = (size_t)length;
+ result = PURE_OK;
+ }
+ }
+ if (result != PURE_OK) free(all);
+ close(fd);
+ return result;
+}
+
+PureResult pure_image_remove(const char *directory, const char *name) {
+ char final[64];
+ if (!image_name(final, name, "")) return PURE_INVALID;
+ int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (dir < 0) return PURE_STORE_ERROR;
+ bool ok = (!unlinkat(dir, final, 0) || errno == ENOENT) && sync_whole(dir);
+ close(dir);
+ return ok ? PURE_OK : PURE_STORE_ERROR;
+}