Preserve Queue entries across spool failures
Debian Trixie package bundle / packages (push) Failing after 13m33s

This commit is contained in:
Mario Fetka
2026-07-23 08:59:42 +02:00
parent 5aeccb4fe7
commit 4bcc8b6476
8 changed files with 576 additions and 48 deletions
+14
View File
@@ -94,6 +94,20 @@ reserve was subtracted without 32-bit truncation:
./contrib/testing/queue-disk-space-check.sh
```
The Queue failure-safety check retains one isolated message while it first
raises the operative reserve above the filesystem's available bytes and then
makes the spool unreadable. It verifies rejection/defer responses, restores
the configuration and permissions through traps, and compares the retained
message byte-for-byte after both recoveries:
```sh
./contrib/testing/queue-failure-safety-check.sh
```
It requires passwordless permission for `bongo-admin __config-read` and
`__config-replace`, Bongo service restarts, and `stat`/`chmod` on the
dedicated test spool. Run it only on a disposable test installation.
The Debian source probes and package-build commands used by BLD-14/BLD-15 are
consolidated in `contrib/debian/build-trixie-bundle.sh`; they are not maintained
as duplicate container-only snippets here.
+139
View File
@@ -0,0 +1,139 @@
#!/bin/sh
#
# Verify that a simulated full Queue filesystem rejects new submissions and
# that an unreadable spool defers access without changing an existing message.
set -eu
queue_tool=${BONGO_QUEUE_TOOL:-/usr/bin/bongo-queuetool}
admin_tool=${BONGO_ADMIN_TOOL:-/usr/bin/bongo-admin}
service=${BONGO_SERVICE:-bongo.service}
spool=${BONGO_QUEUE_SPOOL:-/var/lib/bongo/spool}
run_as=${BONGO_TEST_RUN_AS:-bongo}
as_root=${BONGO_TEST_AS_ROOT:-sudo -n}
if [ "$(id -un)" = "$run_as" ]; then
as_bongo=
else
as_bongo="sudo -n -u $run_as"
fi
work=$(mktemp -d "${TMPDIR:-/tmp}/bongo-stq04.XXXXXX")
chmod 0755 "$work"
queue_id=
spool_mode=
config_changed=0
restore()
{
status=$?
if [ -n "$spool_mode" ]; then
$as_root /usr/bin/chmod "$spool_mode" "$spool" >/dev/null 2>&1 || true
fi
if [ "$config_changed" -eq 1 ]; then
$as_root "$admin_tool" __config-replace queue \
<"$work/queue.original" >/dev/null 2>&1 || true
$as_root /usr/bin/systemctl restart "$service" >/dev/null 2>&1 || true
fi
if [ -n "$queue_id" ]; then
$as_bongo "$queue_tool" delete "$queue_id" >/dev/null 2>&1 || true
fi
rm -rf "$work"
exit "$status"
}
trap restore EXIT HUP INT TERM
printf '%s\r\n' \
'From: STQ-04 <stq04@bongo.test>' \
'To: stqqueue@bongo.test' \
'Subject: STQ-04 retained queue fixture' \
'Date: Thu, 23 Jul 2026 12:34:56 +0200' \
'Message-ID: <stq04-retained@bongo.test>' \
'' \
'This exact message must survive both simulated failures.' \
>"$work/message.eml"
chmod 0644 "$work/message.eml"
$as_root "$admin_tool" __config-read queue >"$work/queue.original"
queue_id=$($as_bongo "$queue_tool" hold-local "$work/message.eml")
$as_bongo "$queue_tool" message "$queue_id" >"$work/before.eml"
before_hash=$(sha256sum "$work/before.eml" | awk '{ print $1 }')
set -- $($as_bongo "$queue_tool" space details)
free_before=$1
free_after=$2
block=$5
python3 - "$work/queue.original" "$work/queue.full" \
"$free_before" "$free_after" "$block" <<'PY'
import json
import sys
source, destination = sys.argv[1:3]
free_before, free_after, block = map(int, sys.argv[3:])
with open(source, "r", encoding="utf-8") as stream:
config = json.load(stream)
config["minimumfreespace"] = max(free_before, free_after) + block + 1
with open(destination, "w", encoding="utf-8") as stream:
json.dump(config, stream, indent=2, sort_keys=True)
stream.write("\n")
PY
$as_root "$admin_tool" __config-replace queue <"$work/queue.full"
config_changed=1
$as_root /usr/bin/systemctl restart "$service"
reported=$($as_bongo "$queue_tool" space)
if [ "$reported" -ne 0 ]; then
echo "STQ-04: simulated full spool reported $reported usable bytes" >&2
exit 1
fi
if $as_bongo "$queue_tool" hold-local "$work/message.eml" \
>"$work/full.out" 2>"$work/full.err"; then
echo "STQ-04: Queue accepted a message below its free-space reserve" >&2
exit 1
fi
if ! grep -F 'Free disk space too low' "$work/full.err" >/dev/null; then
echo "STQ-04: full-spool rejection did not report the expected error" >&2
sed -n '1,20p' "$work/full.err" >&2
exit 1
fi
$as_bongo "$queue_tool" message "$queue_id" >"$work/after-full.eml"
$as_root "$admin_tool" __config-replace queue <"$work/queue.original"
config_changed=0
$as_root /usr/bin/systemctl restart "$service"
spool_mode=$($as_root /usr/bin/stat -c %a "$spool")
$as_root /usr/bin/chmod 000 "$spool"
if $as_bongo "$queue_tool" message "$queue_id" \
>"$work/unreadable.out" 2>"$work/unreadable.err"; then
echo "STQ-04: Queue read a deliberately unreadable spool" >&2
exit 1
fi
if ! grep -F "Can't read queue data" "$work/unreadable.err" >/dev/null; then
echo "STQ-04: unreadable spool did not report the expected error" >&2
sed -n '1,20p' "$work/unreadable.err" >&2
exit 1
fi
$as_root /usr/bin/chmod "$spool_mode" "$spool"
spool_mode=
$as_bongo "$queue_tool" message "$queue_id" >"$work/after-unreadable.eml"
if ! cmp -s "$work/before.eml" "$work/after-full.eml" ||
! cmp -s "$work/before.eml" "$work/after-unreadable.eml"; then
echo "STQ-04: retained Queue message changed during a failure" >&2
exit 1
fi
after_hash=$(sha256sum "$work/after-unreadable.eml" | awk '{ print $1 }')
if [ "$before_hash" != "$after_hash" ]; then
echo "STQ-04: retained Queue message digest changed" >&2
exit 1
fi
if [ "$($as_root /usr/bin/systemctl is-active "$service")" != active ]; then
echo "STQ-04: Bongo service is not active after recovery" >&2
exit 1
fi
printf 'STQ-04 PASS queue=%s sha256=%s full-reported=%s spool-mode=%s\n' \
"$queue_id" "$after_hash" "$reported" \
"$($as_root /usr/bin/stat -c %a "$spool")"
+6
View File
@@ -38,6 +38,12 @@ for the configured retry path. Likewise, a per-recipient LMTP temporary error
must preserve only that recipient for retry and must not redeliver recipients
which already returned a final success.
Queue control-file rewrites use a same-directory work file and an atomic
rename. The original is retained unless every buffered write and close
succeeds. A failed submission flush or final rename is rejected and its
partial entry discarded, so the submitting SMTP peer remains responsible for
retrying the message instead of receiving a false success.
## Administration and shutdown
Use `bongo-queuetool` to inspect and perform documented queue operations; do
+10
View File
@@ -7,6 +7,7 @@ add_executable(bongoqueue
queued.c
mime.c
queue.c
spool-file.c
store-delivery-format.c
)
@@ -56,4 +57,13 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME queue-store-delivery-format COMMAND queue-store-delivery-format-test)
add_executable(queue-spool-file-test
tests/spool-file-test.c
spool-file.c
)
target_include_directories(queue-spool-file-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME queue-spool-file COMMAND queue-spool-file-test)
endif()
+137 -48
View File
@@ -32,6 +32,7 @@
#include "messages.h"
#include "dsn-format.h"
#include "envelope-format.h"
#include "spool-file.h"
#include "store-delivery-format.h"
static BOOL
@@ -133,10 +134,17 @@ int
fclose_check(FILE **fh, int line)
{
int ret;
ret = fclose(*fh);
FILE *stream;
if (fh == NULL || *fh == NULL) {
errno = EINVAL;
return -1;
}
stream = *fh;
*fh = NULL;
ret = fclose(stream);
if (ret == 0) {
// LogFailureF("File close: handle %ld", *fh);
*fh = NULL;
// LogFailureF("File close: handle %ld", stream);
} else {
LogFailureF("File close failed on line %d: %d", line, errno);
}
@@ -951,12 +959,15 @@ StartOver:
MemFree(qEnvelope);
FCLOSE_CHECK(newFH);
UNLINK_CHECK(path);
QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue);
RENAME_CHECK(path2, path);
if (BongoQueueFileCommit(&newFH, path2, path) != 0) {
Log(LOG_WARNING,
"Could not atomically replace queue control file "
"'%s': %s; original retained",
path, strerror(errno));
ProcessQueueEntryCleanUp(entryID, report);
return(TRUE);
}
break;
}
@@ -3069,41 +3080,53 @@ CommandQaddq(void *param)
return(ConnWrite(client->conn, MSG1000ENTRYMADE, sizeof(MSG1000ENTRYMADE) - 1));
}
int
CommandQabrt(void *param)
static void
DiscardClientEntry(QueueClient *client)
{
int ccode;
QueueClient *client = (QueueClient *)param;
if (client->entry.control) {
FCLOSE_CHECK(client->entry.control);
client->entry.control = NULL;
QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id);
UNLINK_CHECK(client->path);
}
if (client->entry.id) {
QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id);
if (unlink(client->path) != 0 && errno != ENOENT) {
LogFailureF("Unable to discard queue control file %s: %d",
client->path, errno);
}
}
if (client->entry.data) {
FCLOSE_CHECK(client->entry.data);
client->entry.data = NULL;
}
if (client->entry.id) {
QueueFormat(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id);
UNLINK_CHECK(client->path);
if (unlink(client->path) != 0 && errno != ENOENT) {
LogFailureF("Unable to discard queue data file %s: %d",
client->path, errno);
}
}
if (client->entry.work) {
FCLOSE_CHECK(client->entry.work);
client->entry.work = NULL;
if (client->entry.workQueue[0] != '\0') {
QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue);
UNLINK_CHECK(client->path);
}
if (client->entry.workQueue[0] != '\0') {
QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue);
if (unlink(client->path) != 0 && errno != ENOENT) {
LogFailureF("Unable to discard queue work file %s: %d",
client->path, errno);
}
}
client->entry.id = 0;
ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1);
return(ccode);
client->entry.target = 0;
client->entry.workQueue[0] = '\0';
}
int
CommandQabrt(void *param)
{
QueueClient *client = (QueueClient *)param;
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
int
@@ -3375,7 +3398,15 @@ CommandQcrea(void *param)
QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id);
FOPEN_CHECK(client->entry.control, client->path, "wb");
if (client->entry.control) {
fprintf(client->entry.control, QUEUES_DATE"%" PRIdMAX "\r\n", (intmax_t)time(NULL));
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_DATE"%" PRIdMAX "\r\n",
(intmax_t)time(NULL)) != 0 ||
BongoQueueFileFlush(client->entry.control) != 0) {
FCLOSE_CHECK(client->entry.control);
unlink(client->path);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
} else {
return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1));
}
@@ -4184,18 +4215,34 @@ CommandQrun(void *param)
/* QRUN[ <queue>-<id>] */
if (*ptr == '\0') {
if (client->entry.control && client->entry.data && client->entry.id) {
FCLOSE_CHECK(client->entry.data);
client->entry.data = NULL;
int dataStatus = BongoQueueFileFinish(&client->entry.data);
int controlStatus =
BongoQueueFileFinish(&client->entry.control);
FCLOSE_CHECK(client->entry.control);
client->entry.control = NULL;
if (dataStatus != 0 || controlStatus != 0) {
Log(LOG_ERROR,
"Could not flush queue entry %07lx: %s; "
"submission rejected",
client->entry.id, strerror(errno));
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
} else {
return(ConnWrite(client->conn, MSG4000CANTUNLOCKENTRY, sizeof(MSG4000CANTUNLOCKENTRY) - 1));
}
QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id);
QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target);
RENAME_CHECK(client->path, path);
if (rename(client->path, path) != 0) {
Log(LOG_ERROR,
"Could not commit queue entry %07lx: %s; "
"submission rejected",
client->entry.id, strerror(errno));
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
XplSafeIncrement(Queue.queuedLocal);
@@ -4571,7 +4618,13 @@ CommandQstorCal(void *param)
/* QSTOR CAL <recipient>[ <calendar name>][ <routing envelope flags>] */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control, QUEUES_CALENDAR_LOCAL"%s\r\n", ptr);
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_CALENDAR_LOCAL"%s\r\n",
ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
@@ -4594,7 +4647,12 @@ CommandQstorFlags(void *param)
/* QSTOR FLAGS <value> */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control, QUEUES_FLAGS"%s\r\n", ptr);
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_FLAGS"%s\r\n", ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
@@ -4616,7 +4674,12 @@ CommandQstorFrom(void *param)
/* QSTOR FROM <sender> <authenticated sender | -> */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control, QUEUES_FROM"%s\r\n", ptr);
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_FROM"%s\r\n", ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
@@ -4639,7 +4702,13 @@ CommandQstorLocal(void *param)
/* QSTOR LOCAL <recipient> <original recipient> <routing envelope flags> */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control, QUEUES_RECIP_LOCAL"%s\r\n", ptr);
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_RECIP_LOCAL"%s\r\n",
ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
@@ -4678,15 +4747,19 @@ CommandQstorMessage(void *param)
}
MsgGetRFC822Date(-1, 0, TimeBuf);
fprintf(client->entry.data,
"Received: from %s (%d.%d.%d.%d) by %s\r\n\twith NMAP (bongoqueue Agent); %s\r\n",
BongoGlobals.hostname, /* FIXME: this should eventually be the remote hsotname of the agent */
if (BongoQueueFilePrintf(
client->entry.data,
"Received: from %s (%d.%d.%d.%d) by %s\r\n"
"\twith NMAP (bongoqueue Agent); %s\r\n",
BongoGlobals.hostname,
client->conn->socketAddress.sin_addr.s_net,
client->conn->socketAddress.sin_addr.s_host,
client->conn->socketAddress.sin_addr.s_lh,
client->conn->socketAddress.sin_addr.s_impno,
BongoGlobals.hostname,
TimeBuf);
BongoGlobals.hostname, TimeBuf) != 0) {
DiscardClientEntry(client);
return -1;
}
/*
fprintf(client->entry.data,
"Received: from %d.%d.%d.%d [%d.%d.%d.%d] by %s\r\n\twith NMAP (%s); %s\r\n",
@@ -4710,8 +4783,14 @@ CommandQstorMessage(void *param)
ccode = ConnReadToFileUntilEOS(client->conn, client->entry.data);
}
if (ccode != -1) {
if (ccode != -1 && BongoQueueFileFlush(client->entry.data) == 0) {
ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1);
} else {
DiscardClientEntry(client);
if (ccode != -1) {
ccode = ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1);
}
}
return(ccode);
@@ -4732,7 +4811,11 @@ CommandQstorRaw(void *param)
/* QSTOR RAW <value> */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control,"%s\r\n", ptr);
if (BongoQueueFilePrintf(client->entry.control, "%s\r\n", ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
@@ -4755,7 +4838,13 @@ CommandQstorTo(void *param)
/* QSTOR TO <value> */
if ((*ptr++ == ' ')
&& (!isspace(*ptr))) {
fprintf(client->entry.control,QUEUES_RECIP_REMOTE"%s\r\n", ptr);
if (BongoQueueFilePrintf(
client->entry.control, QUEUES_RECIP_REMOTE"%s\r\n",
ptr) != 0) {
DiscardClientEntry(client);
return(ConnWrite(client->conn, MSG5221SPACELOW,
sizeof(MSG5221SPACELOW) - 1));
}
return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1));
}
+128
View File
@@ -0,0 +1,128 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <unistd.h>
#include "spool-file.h"
int
BongoQueueFileFlush(FILE *stream)
{
if (stream == NULL) {
errno = EINVAL;
return -1;
}
if (ferror(stream) || fflush(stream) != 0 || ferror(stream)) {
if (errno == 0) errno = EIO;
return -1;
}
return 0;
}
int
BongoQueueFileFinish(FILE **stream)
{
FILE *openStream;
int savedError = 0;
if (stream == NULL || *stream == NULL) {
errno = EINVAL;
return -1;
}
openStream = *stream;
*stream = NULL;
if (BongoQueueFileFlush(openStream) != 0) savedError = errno;
if (fclose(openStream) != 0 && savedError == 0) savedError = errno;
if (savedError != 0) {
errno = savedError;
return -1;
}
return 0;
}
int
BongoQueueFileCommit(FILE **stream, const char *workPath,
const char *destinationPath)
{
int savedError;
if (stream == NULL || *stream == NULL || workPath == NULL ||
destinationPath == NULL) {
errno = EINVAL;
return -1;
}
if (BongoQueueFileFinish(stream) != 0) {
savedError = errno;
unlink(workPath);
errno = savedError;
return -1;
}
if (rename(workPath, destinationPath) != 0) {
savedError = errno;
unlink(workPath);
errno = savedError;
return -1;
}
return 0;
}
int
BongoQueueFileWrite(FILE *stream, const void *data, size_t length)
{
if (stream == NULL || (data == NULL && length != 0U)) {
errno = EINVAL;
return -1;
}
if (length != 0U && fwrite(data, 1U, length, stream) != length) {
if (errno == 0) errno = EIO;
return -1;
}
return 0;
}
int
BongoQueueFileVPrintf(FILE *stream, const char *format, va_list arguments)
{
if (stream == NULL || format == NULL) {
errno = EINVAL;
return -1;
}
if (vfprintf(stream, format, arguments) < 0 || ferror(stream)) {
if (errno == 0) errno = EIO;
return -1;
}
return 0;
}
int
BongoQueueFilePrintf(FILE *stream, const char *format, ...)
{
va_list arguments;
int result;
va_start(arguments, format);
result = BongoQueueFileVPrintf(stream, format, arguments);
va_end(arguments);
return result;
}
+38
View File
@@ -0,0 +1,38 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#ifndef BONGO_QUEUE_SPOOL_FILE_H
#define BONGO_QUEUE_SPOOL_FILE_H
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
int BongoQueueFileFlush(FILE *stream);
int BongoQueueFileFinish(FILE **stream);
int BongoQueueFileCommit(FILE **stream, const char *workPath,
const char *destinationPath);
int BongoQueueFileWrite(FILE *stream, const void *data, size_t length);
int BongoQueueFileVPrintf(FILE *stream, const char *format,
va_list arguments);
int BongoQueueFilePrintf(FILE *stream, const char *format, ...);
#endif
+104
View File
@@ -0,0 +1,104 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "spool-file.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "check failed at line %d: %s\n", \
__LINE__, #condition); \
return 1; \
} \
} while (0)
static int
ReadText(const char *path, char *buffer, size_t size)
{
FILE *stream = fopen(path, "rb");
size_t length;
if (stream == NULL) return -1;
length = fread(buffer, 1U, size - 1U, stream);
buffer[length] = '\0';
return fclose(stream);
}
int
main(void)
{
char directory[] = "/tmp/bongo-queue-spool-test.XXXXXX";
char destination[256];
char missingDestination[256];
char work[256];
char buffer[32];
FILE *stream;
CHECK(mkdtemp(directory) != NULL);
snprintf(destination, sizeof(destination), "%s/control", directory);
snprintf(work, sizeof(work), "%s/work", directory);
snprintf(missingDestination, sizeof(missingDestination),
"%s/missing/control", directory);
stream = fopen(destination, "wb");
CHECK(stream != NULL);
CHECK(BongoQueueFileWrite(stream, "original", 8U) == 0);
CHECK(BongoQueueFileFinish(&stream) == 0);
CHECK(stream == NULL);
stream = fopen(work, "wb");
CHECK(stream != NULL);
CHECK(BongoQueueFilePrintf(stream, "%s", "replacement") == 0);
CHECK(BongoQueueFileCommit(&stream, work, destination) == 0);
CHECK(stream == NULL);
CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0);
CHECK(strcmp(buffer, "replacement") == 0);
stream = fopen(work, "wb");
CHECK(stream != NULL);
CHECK(BongoQueueFileWrite(stream, "discard", 7U) == 0);
CHECK(BongoQueueFileCommit(
&stream, work, missingDestination) != 0);
CHECK(access(work, F_OK) != 0);
CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0);
CHECK(strcmp(buffer, "replacement") == 0);
if (access("/dev/full", W_OK) == 0) {
CHECK(symlink("/dev/full", work) == 0);
stream = fopen(work, "wb");
CHECK(stream != NULL);
CHECK(BongoQueueFilePrintf(stream, "%s", "must fail") == 0);
CHECK(BongoQueueFileCommit(&stream, work, destination) != 0);
CHECK(stream == NULL);
CHECK(access(work, F_OK) != 0);
CHECK(ReadText(destination, buffer, sizeof(buffer)) == 0);
CHECK(strcmp(buffer, "replacement") == 0);
}
CHECK(unlink(destination) == 0);
CHECK(rmdir(directory) == 0);
return 0;
}