From 4b542416a6e0f57b9892ca4a3ca83c887191277c Mon Sep 17 00:00:00 2001 From: chri50 Date: Fri, 17 Oct 2025 14:40:10 +0200 Subject: [PATCH 1/3] DC - Add MIME sink pattern reuse cache. --- scheduler/Makefile | 1 + scheduler/mime-sink-patterns.c | 498 +++++++++++++++++++++++++++++++++ scheduler/mime-sink-patterns.h | 34 +++ scheduler/printers.c | 18 ++ 4 files changed, 551 insertions(+) create mode 100644 scheduler/mime-sink-patterns.c create mode 100644 scheduler/mime-sink-patterns.h diff --git a/scheduler/Makefile b/scheduler/Makefile index 0ec0585837..6b97a5f90b 100644 --- a/scheduler/Makefile +++ b/scheduler/Makefile @@ -36,6 +36,7 @@ CUPSDOBJS = \ server.o \ statbuf.o \ subscriptions.o \ + mime-sink-patterns.c \ sysman.o LIBOBJS = \ filter.o \ diff --git a/scheduler/mime-sink-patterns.c b/scheduler/mime-sink-patterns.c new file mode 100644 index 0000000000..c627e51548 --- /dev/null +++ b/scheduler/mime-sink-patterns.c @@ -0,0 +1,498 @@ +/* + * Implementation of printer sink pattern reuse. + * + * This cache recognizes when multiple printers share the same MIME filter + * configuration and reuses the supported format list instead of recomputing + * it for each printer.The signature includes all filter edges (including + * printer-specific filters normalized to printer/sink) with their costs, + * maxsize limits, and program hashes to ensure correct cache sharing. + * + */ + +#include "cupsd.h" +#include "mime-sink-patterns.h" +#include + +/* FNV-1a hash constants (fast non-cryptographic hash for hash tables) */ +#define FNV1A_32_INIT 0x811c9dc5u /* 2166136261u */ +#define FNV1A_32_PRIME 0x01000193u /* 16777619u */ + +/* Field separators for hash collision prevention */ +#define HASH_SEP_SUPER 0xff /* Separator after super type */ +#define HASH_SEP_TYPE 0xfe /* Separator after type */ + +/* Bit masks */ +#define UINT32_MASK 0xFFFFFFFFU /* 32-bit mask for size_t truncation */ +#define BYTE_MASK 0xff /* Byte extraction mask */ + +/* + * An edge represents a MIME type conversion filter: + * from source type (super/type) to destination, with cost and size limits + */ +typedef struct msink_edge_s +{ + const char *super; /* Source MIME super-type (e.g., "application") */ + const char *type; /* Source MIME type (e.g., "pdf") */ + int cost; /* Conversion cost */ + size_t maxsize; /* Maximum file size for this filter */ + uint32_t prog_hash; /* Hash of the filter program path */ +} msink_edge_t; + +/* + * Cache entry: stores a signature and the list of supported MIME types + * for printers that share the same filter configuration + */ +typedef struct msink_entry_s +{ + uint32_t sig; /* Signature hash of the edge configuration */ + int edge_count; /* Number of edges in canonical list */ + msink_edge_t *edges; /* Canonical sorted edge list */ + cups_array_t *filetypes; /* Supported MIME types (mime_type_t*) */ + struct msink_entry_s *next; /* Next entry in hash bucket chain */ +} msink_entry_t; + +#define MSINK_HT_SIZE 1024 + +/* Global state */ +static msink_entry_t *msink_table[MSINK_HT_SIZE]; +static int msink_enabled_checked = 0; +static int msink_enabled = 0; + + +/* + * 'msink_env_enabled()' - Check if feature is enabled via environment variable. + */ + +static int +msink_env_enabled(void) +{ + if (!msink_enabled_checked) + { + const char *env_value = getenv("CUPS_MIME_SINK_REUSE"); + if (env_value && + (!_cups_strcasecmp(env_value, "1") || + !_cups_strcasecmp(env_value, "yes") || + !_cups_strcasecmp(env_value, "true") || + !_cups_strcasecmp(env_value, "on"))) + { + msink_enabled = 1; + } + + msink_enabled_checked = 1; + + /* Log feature state once at startup */ + cupsdLogMessage(CUPSD_LOG_INFO, "CUPS_MIME_SINK_REUSE=%s (%s)", + env_value ? env_value : "(unset)", + msink_enabled ? "enabled" : "disabled"); + } + + return msink_enabled; +} + + +/* + * 'msink_is_enabled()' - Check if sink pattern reuse is enabled. + */ + +int +msink_is_enabled(void) +{ + return msink_env_enabled(); +} + + +/* + * 'hash_str()' - Hash a string using FNV-1a algorithm. + */ + +static uint32_t +hash_str(const char *s) +{ + uint32_t hash = FNV1A_32_INIT; + unsigned char c; + + while (s && (c = (unsigned char)*s++)) + { + hash ^= c; + hash *= FNV1A_32_PRIME; + } + + return hash; +} + + +/* + * 'edge_cmp()' - Compare two edges for sorting. + */ + +static int +edge_cmp(const void *a, const void *b) +{ + const msink_edge_t *edge_a = (const msink_edge_t *)a; + const msink_edge_t *edge_b = (const msink_edge_t *)b; + int diff; + + /* Compare super type */ + if ((diff = strcmp(edge_a->super, edge_b->super)) != 0) + return diff; + + /* Compare type */ + if ((diff = strcmp(edge_a->type, edge_b->type)) != 0) + return diff; + + /* Compare cost */ + if (edge_a->cost != edge_b->cost) + return edge_a->cost - edge_b->cost; + + /* Compare maxsize */ + if (edge_a->maxsize != edge_b->maxsize) + return (edge_a->maxsize > edge_b->maxsize) ? 1 : -1; + + /* Compare program hash */ + if (edge_a->prog_hash != edge_b->prog_hash) + return (int)edge_a->prog_hash - (int)edge_b->prog_hash; + + return 0; +} + + +/* + * 'sig_hash()' - Compute signature hash for edge array using FNV-1a. + */ + +static uint32_t +sig_hash(msink_edge_t *edges, int edge_count) +{ + uint32_t hash = FNV1A_32_INIT; + int i; + + for (i = 0; i < edge_count; i++) + { + const msink_edge_t *edge = &edges[i]; + const unsigned char *str; + + /* Hash the super type string */ + for (str = (const unsigned char *)edge->super; str && *str; str++) + { + hash ^= *str; + hash *= FNV1A_32_PRIME; + } + + /* Field separator to prevent collisions (e.g., "ab"+"c" vs "a"+"bc") */ + hash ^= HASH_SEP_SUPER; + hash *= FNV1A_32_PRIME; + + /* Hash the type string */ + for (str = (const unsigned char *)edge->type; str && *str; str++) + { + hash ^= *str; + hash *= FNV1A_32_PRIME; + } + + /* Field separator */ + hash ^= HASH_SEP_TYPE; + hash *= FNV1A_32_PRIME; + + /* Hash numeric fields: cost, maxsize, prog_hash */ + uint32_t numeric_mix = (uint32_t)edge->cost ^ + (uint32_t)(edge->maxsize & UINT32_MASK) ^ + edge->prog_hash; + + /* Mix in each byte of the numeric fields */ + int byte; + for (byte = 0; byte < 4; byte++) + { + hash ^= (unsigned char)((numeric_mix >> (byte * 8)) & BYTE_MASK); + hash *= FNV1A_32_PRIME; + } + } + + return hash; +} + + +/* + * 'edges_equal()' - Compare two edge arrays for equality. + */ + +static int +edges_equal(msink_edge_t *a, msink_edge_t *b, int count) +{ + int i; + + for (i = 0; i < count; i++) + { + if (strcmp(a[i].super, b[i].super) != 0 || + strcmp(a[i].type, b[i].type) != 0 || + a[i].cost != b[i].cost || + a[i].maxsize != b[i].maxsize || + a[i].prog_hash != b[i].prog_hash) + { + return 0; + } + } + + return 1; +} + + +/* + * 'msink_reuse()' - Try to reuse cached filetypes for a sink. + */ + +int +msink_reuse(mime_t *mime, mime_type_t *sink, cups_array_t **out_filetypes) +{ + if (out_filetypes) *out_filetypes = NULL; + if (!mime || !sink) return 0; + + /* Collect all incoming edges */ + int cap = 8, acnt = 0; + msink_edge_t *all = (msink_edge_t *)malloc(cap * sizeof(msink_edge_t)); + if (!all) return 0; + + mime_filter_t *flt; + for (flt = mimeFirstFilter(mime); flt; flt = mimeNextFilter(mime)) + { + if (flt->dst == sink) + { + if (acnt == cap) + { + cap *= 2; + msink_edge_t *ne = (msink_edge_t *)realloc(all, cap * sizeof(msink_edge_t)); + if (!ne) + { + free(all); + return 0; + } + all = ne; + } + all[acnt].super = flt->src->super; + all[acnt].type = flt->src->type; + all[acnt].cost = flt->cost; + all[acnt].maxsize = flt->maxsize; + all[acnt].prog_hash = hash_str(flt->filter); + acnt++; + } + } + if (acnt == 0) + { + free(all); + return 0; + } + /* Build signature edges (normalize printer/asterix to printer/sink) */ + msink_edge_t *gen = (msink_edge_t *)malloc(acnt * sizeof(msink_edge_t)); + if (!gen) + { + free(all); + return 0; + } + int gcnt = 0; + for (int i = 0; i < acnt; i++) + { + gen[gcnt] = all[i]; /* Copy the edge */ + + /* Normalize printer/asterix sources to printer/sink for signature calculation + * This ensures printers with different printer-specific filter chains + * (different costs, programs, or maxsize) get different signatures, + * while still allowing sharing when the filter behavior is identical. */ + if (!_cups_strcasecmp(all[i].super, "printer")) + { + gen[gcnt].super = "printer"; + gen[gcnt].type = "sink"; + } + + gcnt++; + } + qsort(gen, gcnt, sizeof(msink_edge_t), edge_cmp); + uint32_t sig = sig_hash(gen, gcnt); + unsigned bucket = (unsigned)(sig % MSINK_HT_SIZE); + + /* Search the hash bucket for a matching entry */ + msink_entry_t *ent; + for (ent = msink_table[bucket]; ent; ent = ent->next) + { + if (ent->edge_count == gcnt && edges_equal(ent->edges, gen, gcnt)) + { + /* Found a match - copy the cached filetypes */ + if (out_filetypes) + { + *out_filetypes = cupsArrayNew(NULL, NULL); + if (*out_filetypes) + { + mime_type_t *mt; + for (mt = (mime_type_t *)cupsArrayFirst(ent->filetypes); + mt; + mt = (mime_type_t *)cupsArrayNext(ent->filetypes)) + { + cupsArrayAdd(*out_filetypes, mt); + } + } + } + + free(gen); + free(all); + cupsdLogMessage(CUPSD_LOG_DEBUG2, + "sink-pattern: cache hit signature=%u edges=%d (printer/* normalized)", + sig, gcnt); + return 1; + } + } + + /* No match found in cache */ + free(gen); + free(all); + return 0; +} + + +/* + * 'msink_try_store()' - Store filetypes in cache for future reuse. + */ + +void +msink_try_store(mime_t *mime, mime_type_t *sink, cups_array_t *filetypes) +{ + if (!mime || !sink || !filetypes) return; + + int cap = 8, cnt = 0; + msink_edge_t *all = (msink_edge_t *)malloc(cap * sizeof(msink_edge_t)); + if (!all) return; + + mime_filter_t *flt; + for (flt = mimeFirstFilter(mime); flt; flt = mimeNextFilter(mime)) + { + if (flt->dst == sink) + { + if (cnt == cap) + { + cap *= 2; + msink_edge_t *ne = (msink_edge_t *)realloc(all, cap * sizeof(msink_edge_t)); + if (!ne) + { + free(all); + return; + } + all = ne; + } + all[cnt].super = flt->src->super; + all[cnt].type = flt->src->type; + all[cnt].cost = flt->cost; + all[cnt].maxsize = flt->maxsize; + all[cnt].prog_hash = hash_str(flt->filter); + cnt++; + } + } + if (cnt == 0) + { + free(all); + return; + } + msink_edge_t *gen = (msink_edge_t *)malloc(cnt * sizeof(msink_edge_t)); + if (!gen) + { + free(all); + return; + } + int gcnt = 0; + for (int i = 0; i < cnt; i++) + { + gen[gcnt] = all[i]; /* Copy the edge */ + + /* Normalize printer/asterix sources to printer/sink for signature calculation */ + if (!_cups_strcasecmp(all[i].super, "printer")) + { + gen[gcnt].super = "printer"; + gen[gcnt].type = "sink"; + } + + gcnt++; + } + qsort(gen, gcnt, sizeof(msink_edge_t), edge_cmp); + uint32_t sig = sig_hash(gen, gcnt); + unsigned bucket = (unsigned)(sig % MSINK_HT_SIZE); + + /* Check if this configuration already exists in the cache */ + msink_entry_t *ent; + for (ent = msink_table[bucket]; ent; ent = ent->next) + { + if (ent->edge_count == gcnt && edges_equal(ent->edges, gen, gcnt)) + { + /* Already cached - nothing to do */ + free(gen); + free(all); + return; + } + } + + /* Create new cache entry */ + ent = (msink_entry_t *)calloc(1, sizeof(msink_entry_t)); + if (!ent) + { + free(gen); + free(all); + return; + } + ent->sig = sig; + ent->edge_count = gcnt; + ent->edges = (msink_edge_t *)malloc(gcnt * sizeof(msink_edge_t)); + if (!ent->edges) + { + free(gen); + free(all); + free(ent); + return; + } + memcpy(ent->edges, gen, gcnt * sizeof(msink_edge_t)); + free(gen); + free(all); + + /* retain a copy of filetypes */ + ent->filetypes = cupsArrayNew(NULL, NULL); + if (ent->filetypes) + { + mime_type_t *mt; + for (mt = (mime_type_t *)cupsArrayFirst(filetypes); mt; mt = (mime_type_t *)cupsArrayNext(filetypes)) + cupsArrayAdd(ent->filetypes, mt); + } + ent->next = msink_table[bucket]; + msink_table[bucket] = ent; + cupsdLogMessage(CUPSD_LOG_INFO, "sink-pattern: store signature=%u edges=%d (printer/* normalized) supported=%d", sig, gcnt, cupsArrayCount(ent->filetypes)); + + /* Only log detailed edge info if debug level is high enough */ + if (LogLevel >= CUPSD_LOG_DEBUG) + { + for (int di = 0; di < gcnt; di++) + { + cupsdLogMessage(CUPSD_LOG_DEBUG, + "sink-pattern: edge[%d]: %s/%s cost=%d max=%zu prog_hash=%u", + di, ent->edges[di].super, ent->edges[di].type, ent->edges[di].cost, + ent->edges[di].maxsize, ent->edges[di].prog_hash); + } + } +} + + +/* + * 'msink_try_reuse()' - Try to reuse cached filetypes for a printer. + */ + +int +msink_try_reuse(cupsd_printer_t *printer) +{ + if (!printer) return 0; + + if (!msink_is_enabled()) return 0; + + cups_array_t *reuse_filetypes = NULL; + + /* Try to find cached filetypes for this printer's sink configuration */ + if (msink_reuse(MimeDatabase, printer->filetype, &reuse_filetypes)) + { + /* Cache hit - set the filetypes, caller will handle IPP attributes */ + printer->filetypes = reuse_filetypes; + return 1; + } + + return 0; +} diff --git a/scheduler/mime-sink-patterns.h b/scheduler/mime-sink-patterns.h new file mode 100644 index 0000000000..f8b6190177 --- /dev/null +++ b/scheduler/mime-sink-patterns.h @@ -0,0 +1,34 @@ +/* + * Printer sink pattern reuse (incoming filter signature) for CUPS scheduler. + * + * This cache recognizes when multiple printers share the same MIME filter + * configuration and reuses the supported format list instead of recomputing + * it for each printer. The signature includes all filter edges (including + * printer-specific filters normalized to printer/sink) with their costs, + * maxsize limits, and program hashes to ensure correct cache sharing. + * + */ + +#ifndef _CUPS_MIME_SINK_PATTERNS_H_ +#define _CUPS_MIME_SINK_PATTERNS_H_ + +#include "mime.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * Functions... + */ + +extern int msink_is_enabled(void); +extern int msink_try_reuse(cupsd_printer_t *printer); +extern void msink_try_store(mime_t *mime, mime_type_t *sink,cups_array_t *filetypes); + +#ifdef __cplusplus +} +#endif + +#endif /* !_CUPS_MIME_SINK_PATTERNS_H_ */ diff --git a/scheduler/printers.c b/scheduler/printers.c index 0469734c26..8dd7184744 100644 --- a/scheduler/printers.c +++ b/scheduler/printers.c @@ -10,6 +10,9 @@ */ #include "cupsd.h" +/* === MOD START: sink pattern reuse headers === */ +#include "mime-sink-patterns.h" /* sink pattern reuse */ +/* === MOD END: sink pattern reuse headers === */ #include #ifdef HAVE_APPLICATIONSERVICES_H # include @@ -3548,6 +3551,12 @@ add_printer_formats(cupsd_printer_t *p) /* I - Printer */ return; } +/* === MOD START: sink pattern reuse attempt (before enumeration begin) === */ + int cache_hit = msink_try_reuse(p); + if (!cache_hit) + { +/* === MOD END: sink pattern reuse attempt (before enumeration begin) === */ + /* * Otherwise, loop through the supported MIME types and see if there * are filters for them... @@ -3580,6 +3589,10 @@ add_printer_formats(cupsd_printer_t *p) /* I - Printer */ cupsdLogPrinter(p, CUPSD_LOG_DEBUG2, "add_printer_formats: %s not supported", mimetype); } +/* === MOD START: sink pattern reuse attempt (before enumeration end) === */ + } +/* === MOD END: sink pattern reuse attempt (before enumeration end) === */ + /* * Add the file formats that can be filtered... */ @@ -3608,6 +3621,11 @@ add_printer_formats(cupsd_printer_t *p) /* I - Printer */ ippAddString(p->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_MIMETYPE), "document-format-preferred", NULL, preferred); +/* === MOD START: Store for reuse by future printers (same sink incoming edges) === */ + if (!cache_hit) + msink_try_store(MimeDatabase, p->filetype, p->filetypes); +/* === MOD END: Store for reuse by future printers (same sink incoming edges) === */ + /* * We only support raw printing if this is not a Tioga PrintJobMgr based * queue and if application/octet-stream is a known type... From c0db82b6cb33a578a6a59bc9959e3de2852f1160 Mon Sep 17 00:00:00 2001 From: chri50 Date: Mon, 20 Oct 2025 10:05:46 +0200 Subject: [PATCH 2/3] DC - correct includes --- scheduler/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scheduler/Makefile b/scheduler/Makefile index 6b97a5f90b..31b36c9b15 100644 --- a/scheduler/Makefile +++ b/scheduler/Makefile @@ -29,6 +29,7 @@ CUPSDOBJS = \ log.o \ network.o \ policy.o \ + mime-sink-patterns.o \ printers.o \ process.o \ quotas.o \ @@ -36,7 +37,6 @@ CUPSDOBJS = \ server.o \ statbuf.o \ subscriptions.o \ - mime-sink-patterns.c \ sysman.o LIBOBJS = \ filter.o \ From 81f11e5cf17b1fbffb9d1a3110418f83005f2af4 Mon Sep 17 00:00:00 2001 From: chri50 Date: Mon, 27 Oct 2025 14:03:28 +0100 Subject: [PATCH 3/3] DC - update mime-sink-patterns to adopt cupsArray, simplification --- scheduler/mime-sink-patterns.c | 614 ++++++++++++++++----------------- 1 file changed, 288 insertions(+), 326 deletions(-) diff --git a/scheduler/mime-sink-patterns.c b/scheduler/mime-sink-patterns.c index c627e51548..46e4eeeb65 100644 --- a/scheduler/mime-sink-patterns.c +++ b/scheduler/mime-sink-patterns.c @@ -1,68 +1,86 @@ /* - * Implementation of printer sink pattern reuse. + * cupsArray-based implementation of printer sink pattern reuse. * - * This cache recognizes when multiple printers share the same MIME filter - * configuration and reuses the supported format list instead of recomputing - * it for each printer.The signature includes all filter edges (including - * printer-specific filters normalized to printer/sink) with their costs, - * maxsize limits, and program hashes to ensure correct cache sharing. - * + * This cache detects when multiple printers share the same MIME filter + * configuration and reuses the supported format list instead of + * recomputing it for each printer. A 64-bit signature is computed over a + * sorted list of filter "edges" (printer-specific filters are normalized + * to "printer/sink"); the signature mixes in the edge super/type, cost, + * maxsize and a program hash to compactly represent the filter graph. + * + * The helper build_msink_entry() constructs an heap-allocated + * msink_entry_t containing edge_count and signature (filetypes is left + * NULL). Callers attach the `filetypes` cups_array_t* and then add the + * entry to the global `msink_arr` cupsArray. The array uses copy/free + * callbacks (msink_arr_copy/msink_arr_free) to duplicate and manage the + * stored entries and their filetypes. After insertion we verify the add + * by finding the stored copy and log success or error. + * + * The compare/hash callbacks are intentionally lightweight and use the + * edge_count and signature for fast lookup; extremely unlikely + * signature collisions are accepted as a trade-off for performance. */ + #include "cupsd.h" #include "mime-sink-patterns.h" +#include #include +#include -/* FNV-1a hash constants (fast non-cryptographic hash for hash tables) */ -#define FNV1A_32_INIT 0x811c9dc5u /* 2166136261u */ -#define FNV1A_32_PRIME 0x01000193u /* 16777619u */ +/* FNV-1a 64-bit constants */ +#define FNV1A_64_INIT 0xcbf29ce484222325ULL +#define FNV1A_64_PRIME 0x00000100000001B3ULL -/* Field separators for hash collision prevention */ -#define HASH_SEP_SUPER 0xff /* Separator after super type */ -#define HASH_SEP_TYPE 0xfe /* Separator after type */ +/* Field separator byte values used when mixing fields into the signature + * Use UINT64_C to ensure they have 64-bit type without needing casts. */ +#define FNV1A_64_SEP_SUPER UINT64_C(0xFF) +#define FNV1A_64_SEP_TYPE UINT64_C(0xFE) +#define FNV1A_64_SEP_PROG UINT64_C(0xFD) -/* Bit masks */ -#define UINT32_MASK 0xFFFFFFFFU /* 32-bit mask for size_t truncation */ -#define BYTE_MASK 0xff /* Byte extraction mask */ +/* FNV-1a hash constants */ +#define FNV1A_32_INIT 0x811c9dc5u +#define FNV1A_32_PRIME 0x01000193u -/* - * An edge represents a MIME type conversion filter: - * from source type (super/type) to destination, with cost and size limits - */ +/* Masks */ +#define UINT8_MASK 0xFFu +#define UINT32_MASK 0xFFFFFFFFU + +/* Hash table size used for cupsArrayNew2/3 */ +#define MSINK_ARR_HASH_SIZE 1024 + +/* Edge structure (same shape as the original file-local type) */ typedef struct msink_edge_s { - const char *super; /* Source MIME super-type (e.g., "application") */ - const char *type; /* Source MIME type (e.g., "pdf") */ - int cost; /* Conversion cost */ - size_t maxsize; /* Maximum file size for this filter */ - uint32_t prog_hash; /* Hash of the filter program path */ + const char *super; + const char *type; + int cost; + size_t maxsize; + const char *prog; } msink_edge_t; -/* - * Cache entry: stores a signature and the list of supported MIME types - * for printers that share the same filter configuration - */ +/* Entry stored in the cupsArray cache */ typedef struct msink_entry_s { - uint32_t sig; /* Signature hash of the edge configuration */ - int edge_count; /* Number of edges in canonical list */ - msink_edge_t *edges; /* Canonical sorted edge list */ - cups_array_t *filetypes; /* Supported MIME types (mime_type_t*) */ - struct msink_entry_s *next; /* Next entry in hash bucket chain */ + int edge_count; /* edge count per filetype used */ + uint64_t signature; /* compact signature for the sorted edge list */ + cups_array_t *filetypes; /* mime_type_t* elements */ } msink_entry_t; -#define MSINK_HT_SIZE 1024 - -/* Global state */ -static msink_entry_t *msink_table[MSINK_HT_SIZE]; +/* Global cups array for cache entries (lazily created) */ +static cups_array_t *msink_arr = NULL; static int msink_enabled_checked = 0; static int msink_enabled = 0; +/* Forward declarations for cupsArray callbacks */ +static int msink_arr_compare(void *a, void *b, void *user_data); +static int msink_arr_hash(void *elem, void *user_data); +static void *msink_arr_copy(void *element, void *user_data); +static void msink_arr_free(void *element, void *user_data); /* - * 'msink_env_enabled()' - Check if feature is enabled via environment variable. + * msink_env_enabled() - read CUPS_MIME_SINK_REUSE environment switch */ - static int msink_env_enabled(void) { @@ -77,23 +95,20 @@ msink_env_enabled(void) { msink_enabled = 1; } - + msink_enabled_checked = 1; - - /* Log feature state once at startup */ + cupsdLogMessage(CUPSD_LOG_INFO, "CUPS_MIME_SINK_REUSE=%s (%s)", env_value ? env_value : "(unset)", msink_enabled ? "enabled" : "disabled"); } - + return msink_enabled; } - /* - * 'msink_is_enabled()' - Check if sink pattern reuse is enabled. + * msink_is_enabled() - public wrapper that returns whether feature is enabled */ - int msink_is_enabled(void) { @@ -102,397 +117,344 @@ msink_is_enabled(void) /* - * 'hash_str()' - Hash a string using FNV-1a algorithm. + * edge_cmp() - compare two msink_edge_t entries for sorting */ - -static uint32_t -hash_str(const char *s) -{ - uint32_t hash = FNV1A_32_INIT; - unsigned char c; - - while (s && (c = (unsigned char)*s++)) - { - hash ^= c; - hash *= FNV1A_32_PRIME; - } - - return hash; -} - - -/* - * 'edge_cmp()' - Compare two edges for sorting. - */ - static int edge_cmp(const void *a, const void *b) { const msink_edge_t *edge_a = (const msink_edge_t *)a; const msink_edge_t *edge_b = (const msink_edge_t *)b; int diff; - - /* Compare super type */ + if ((diff = strcmp(edge_a->super, edge_b->super)) != 0) return diff; - - /* Compare type */ if ((diff = strcmp(edge_a->type, edge_b->type)) != 0) return diff; - - /* Compare cost */ if (edge_a->cost != edge_b->cost) return edge_a->cost - edge_b->cost; - - /* Compare maxsize */ if (edge_a->maxsize != edge_b->maxsize) return (edge_a->maxsize > edge_b->maxsize) ? 1 : -1; - - /* Compare program hash */ - if (edge_a->prog_hash != edge_b->prog_hash) - return (int)edge_a->prog_hash - (int)edge_b->prog_hash; - + { + const char *pa = edge_a->prog ? edge_a->prog : ""; + const char *pb = edge_b->prog ? edge_b->prog : ""; + if ((diff = strcmp(pa, pb)) != 0) + return diff; + } return 0; } - /* - * 'sig_hash()' - Compute signature hash for edge array using FNV-1a. + * compute_edges_signature() - compute 64-bit FNV-1a signature for edges */ - -static uint32_t -sig_hash(msink_edge_t *edges, int edge_count) +static uint64_t +compute_edges_signature(const msink_edge_t *edges, int count) { - uint32_t hash = FNV1A_32_INIT; - int i; - - for (i = 0; i < edge_count; i++) + uint64_t h = FNV1A_64_INIT; + for (int i = 0; i < count; i++) { - const msink_edge_t *edge = &edges[i]; - const unsigned char *str; - - /* Hash the super type string */ - for (str = (const unsigned char *)edge->super; str && *str; str++) + const msink_edge_t *e = &edges[i]; + + /* Mix in super and a separator */ + for (const unsigned char *s = (const unsigned char *)e->super; s && *s; s++) { - hash ^= *str; - hash *= FNV1A_32_PRIME; + h ^= (uint64_t)*s; + h *= FNV1A_64_PRIME; } - - /* Field separator to prevent collisions (e.g., "ab"+"c" vs "a"+"bc") */ - hash ^= HASH_SEP_SUPER; - hash *= FNV1A_32_PRIME; - - /* Hash the type string */ - for (str = (const unsigned char *)edge->type; str && *str; str++) + h ^= FNV1A_64_SEP_SUPER; + h *= FNV1A_64_PRIME; + + /* Mix in type and separator */ + for (const unsigned char *s = (const unsigned char *)e->type; s && *s; s++) { - hash ^= *str; - hash *= FNV1A_32_PRIME; + h ^= (uint64_t)*s; + h *= FNV1A_64_PRIME; } - - /* Field separator */ - hash ^= HASH_SEP_TYPE; - hash *= FNV1A_32_PRIME; - - /* Hash numeric fields: cost, maxsize, prog_hash */ - uint32_t numeric_mix = (uint32_t)edge->cost ^ - (uint32_t)(edge->maxsize & UINT32_MASK) ^ - edge->prog_hash; - - /* Mix in each byte of the numeric fields */ - int byte; - for (byte = 0; byte < 4; byte++) + h ^= FNV1A_64_SEP_TYPE; + h *= FNV1A_64_PRIME; + + /* Mix in numeric fields deterministically */ + uint64_t mix = (uint64_t)(uint32_t)e->cost; + mix = (mix << 32) ^ (uint64_t)(e->maxsize & UINT32_MASK); + for (int b = 0; b < 4; b++) { - hash ^= (unsigned char)((numeric_mix >> (byte * 8)) & BYTE_MASK); - hash *= FNV1A_32_PRIME; + unsigned char byte = (unsigned char)((mix >> (b*8)) & UINT8_MASK); + h ^= (uint64_t)byte; + h *= FNV1A_64_PRIME; } - } - - return hash; -} - -/* - * 'edges_equal()' - Compare two edge arrays for equality. - */ - -static int -edges_equal(msink_edge_t *a, msink_edge_t *b, int count) -{ - int i; - - for (i = 0; i < count; i++) - { - if (strcmp(a[i].super, b[i].super) != 0 || - strcmp(a[i].type, b[i].type) != 0 || - a[i].cost != b[i].cost || - a[i].maxsize != b[i].maxsize || - a[i].prog_hash != b[i].prog_hash) + /* Mix in program string (if any) and separator */ + if (e->prog) { - return 0; + for (const unsigned char *s = (const unsigned char *)e->prog; s && *s; s++) + { + h ^= (uint64_t)*s; + h *= FNV1A_64_PRIME; + } } + h ^= FNV1A_64_SEP_PROG; + h *= FNV1A_64_PRIME; } - - return 1; + return h; } - /* - * 'msink_reuse()' - Try to reuse cached filetypes for a sink. + * build_msink_entry() - construct a temporary msink_entry_t with signature + * + * Allocates and returns a heap msink_entry_t with edge_count and signature + * populated. filetypes is left NULL and callers must free the returned + * entry when finished. */ - -int -msink_reuse(mime_t *mime, mime_type_t *sink, cups_array_t **out_filetypes) +static msink_entry_t * +build_msink_entry(mime_t *mime, mime_type_t *sink) { - if (out_filetypes) *out_filetypes = NULL; - if (!mime || !sink) return 0; + if (!mime || !sink) return NULL; - /* Collect all incoming edges */ - int cap = 8, acnt = 0; + int cap = 8, cnt = 0; msink_edge_t *all = (msink_edge_t *)malloc(cap * sizeof(msink_edge_t)); - if (!all) return 0; + if (!all) return NULL; mime_filter_t *flt; for (flt = mimeFirstFilter(mime); flt; flt = mimeNextFilter(mime)) { if (flt->dst == sink) { - if (acnt == cap) + if (cnt == cap) { cap *= 2; msink_edge_t *ne = (msink_edge_t *)realloc(all, cap * sizeof(msink_edge_t)); if (!ne) { free(all); - return 0; + return NULL; } all = ne; } - all[acnt].super = flt->src->super; - all[acnt].type = flt->src->type; - all[acnt].cost = flt->cost; - all[acnt].maxsize = flt->maxsize; - all[acnt].prog_hash = hash_str(flt->filter); - acnt++; + all[cnt].super = flt->src->super; + all[cnt].type = flt->src->type; + all[cnt].cost = flt->cost; + all[cnt].maxsize = flt->maxsize; + all[cnt].prog = flt->filter; + cnt++; } } - if (acnt == 0) + if (cnt == 0) { free(all); - return 0; + return NULL; } - /* Build signature edges (normalize printer/asterix to printer/sink) */ - msink_edge_t *gen = (msink_edge_t *)malloc(acnt * sizeof(msink_edge_t)); + + msink_edge_t *gen = (msink_edge_t *)malloc(cnt * sizeof(msink_edge_t)); if (!gen) { free(all); - return 0; + return NULL; } int gcnt = 0; - for (int i = 0; i < acnt; i++) + for (int i = 0; i < cnt; i++) { - gen[gcnt] = all[i]; /* Copy the edge */ - - /* Normalize printer/asterix sources to printer/sink for signature calculation - * This ensures printers with different printer-specific filter chains - * (different costs, programs, or maxsize) get different signatures, - * while still allowing sharing when the filter behavior is identical. */ + gen[gcnt] = all[i]; if (!_cups_strcasecmp(all[i].super, "printer")) { gen[gcnt].super = "printer"; gen[gcnt].type = "sink"; } - gcnt++; } qsort(gen, gcnt, sizeof(msink_edge_t), edge_cmp); - uint32_t sig = sig_hash(gen, gcnt); - unsigned bucket = (unsigned)(sig % MSINK_HT_SIZE); + + uint64_t sig = compute_edges_signature(gen, gcnt); + + free(all); + free(gen); + + msink_entry_t *entry = (msink_entry_t *)calloc(1, sizeof(msink_entry_t)); + if (!entry) return NULL; + entry->edge_count = gcnt; + entry->signature = sig; + entry->filetypes = NULL; + return entry; +} + + +/* cupsArray msink_arr ---------------------------------------------------- */ + +/* + * msink_arr_init() - lazy initializer for the global msink_arr + */ +static void +msink_arr_init(void) +{ + if (!msink_arr) + msink_arr = cupsArrayNew3((cups_array_func_t)msink_arr_compare, NULL, + (cups_ahash_func_t)msink_arr_hash, MSINK_ARR_HASH_SIZE, + (cups_acopy_func_t)msink_arr_copy, (cups_afree_func_t)msink_arr_free); +} + +/* cupsArray callbacks ---------------------------------------------------- */ + +/* + * msink_arr_compare() - cupsArray compare callback for msink entries + */ +static int +msink_arr_compare(void *a, void *b, void *user_data) +{ + msink_entry_t *ea = (msink_entry_t *)a; + msink_entry_t *eb = (msink_entry_t *)b; + + if (ea->edge_count < eb->edge_count) return -1; + if (ea->edge_count > eb->edge_count) return 1; + + /* Fast path: compare signatures */ + if (ea->signature < eb->signature) return -1; + if (ea->signature > eb->signature) return 1; + + /* Signatures equal and edge_count equal -> treat as equal. + * If an extremely unlikely collision occurs this will treat as equal. + */ + return 0; +} + +/* +* msink_arr_hash() - cupsArray hash callback (reduce 64-bit sig to bucket) +* +* Use the precomputed 64-bit signature when available. +* Reduce it to the hash table size. +*/ +static int +msink_arr_hash(void *elem, void *user_data) +{ + msink_entry_t *e = (msink_entry_t *)elem; + + /* Fold 64-bit signature to 32-bit and modulo table size */ + uint64_t sig = e->signature; + uint32_t folded = (uint32_t)(sig ^ (sig >> 32)); + return (int)(folded % MSINK_ARR_HASH_SIZE); +} + +/* + * msink_arr_copy() - cupsArray copy callback for msink_entry_t + */ +static void * +msink_arr_copy(void *element, void *user_data) +{ + msink_entry_t *src = (msink_entry_t *)element; + msink_entry_t *dst = (msink_entry_t *)calloc(1, sizeof(msink_entry_t)); + if (!dst) return NULL; + dst->edge_count = src->edge_count; + dst->signature = src->signature; + + dst->filetypes = src->filetypes ? cupsArrayDup(src->filetypes) : NULL; + + return dst; +} + +/* + * msink_arr_free() - cupsArray free callback for msink_entry_t + */ +static void +msink_arr_free(void *element, void *user_data) +{ + msink_entry_t *e = (msink_entry_t *)element; + if (!e) return; + if (e->filetypes) cupsArrayDelete(e->filetypes); + free(e); +} + +/* Private API ---------------------------------------------------- */ + +/* + * msink_reuse() - try to reuse a cached filetypes list for a sink + */ +int +msink_reuse(mime_t *mime, mime_type_t *sink, cups_array_t **out_filetypes) +{ + if (out_filetypes) *out_filetypes = NULL; + if (!mime || !sink) return 0; + + /* Build an msink_entry for lookup */ + msink_entry_t *lookup_sink = build_msink_entry(mime, sink); + if (!lookup_sink) return 0; + + /* Ensure array exists */ + msink_arr_init(); + + msink_entry_t *found = (msink_entry_t *)cupsArrayFind(msink_arr, lookup_sink); + free(lookup_sink); - /* Search the hash bucket for a matching entry */ - msink_entry_t *ent; - for (ent = msink_table[bucket]; ent; ent = ent->next) + if (found) { - if (ent->edge_count == gcnt && edges_equal(ent->edges, gen, gcnt)) - { - /* Found a match - copy the cached filetypes */ - if (out_filetypes) - { - *out_filetypes = cupsArrayNew(NULL, NULL); - if (*out_filetypes) - { - mime_type_t *mt; - for (mt = (mime_type_t *)cupsArrayFirst(ent->filetypes); - mt; - mt = (mime_type_t *)cupsArrayNext(ent->filetypes)) - { - cupsArrayAdd(*out_filetypes, mt); - } - } - } - - free(gen); - free(all); - cupsdLogMessage(CUPSD_LOG_DEBUG2, - "sink-pattern: cache hit signature=%u edges=%d (printer/* normalized)", - sig, gcnt); - return 1; - } + if (out_filetypes && found->filetypes) + *out_filetypes = cupsArrayDup(found->filetypes); + + cupsdLogMessage(CUPSD_LOG_DEBUG2, + "sink-pattern-arr: cache hit edges=%d (printer/* normalized)", + found->edge_count); + return 1; } - - /* No match found in cache */ - free(gen); - free(all); return 0; } +/* Public API ---------------------------------------------------- */ /* - * 'msink_try_store()' - Store filetypes in cache for future reuse. + * msink_try_store() - try to store a filetypes list in the cache */ - void msink_try_store(mime_t *mime, mime_type_t *sink, cups_array_t *filetypes) { if (!mime || !sink || !filetypes) return; + if (!msink_is_enabled()) return; - int cap = 8, cnt = 0; - msink_edge_t *all = (msink_edge_t *)malloc(cap * sizeof(msink_edge_t)); - if (!all) return; + /* Generating the edge signature */ + msink_entry_t *lookup_sink = build_msink_entry(mime, sink); + if (!lookup_sink) return; - mime_filter_t *flt; - for (flt = mimeFirstFilter(mime); flt; flt = mimeNextFilter(mime)) - { - if (flt->dst == sink) - { - if (cnt == cap) - { - cap *= 2; - msink_edge_t *ne = (msink_edge_t *)realloc(all, cap * sizeof(msink_edge_t)); - if (!ne) - { - free(all); - return; - } - all = ne; - } - all[cnt].super = flt->src->super; - all[cnt].type = flt->src->type; - all[cnt].cost = flt->cost; - all[cnt].maxsize = flt->maxsize; - all[cnt].prog_hash = hash_str(flt->filter); - cnt++; - } - } - if (cnt == 0) - { - free(all); - return; - } - msink_edge_t *gen = (msink_edge_t *)malloc(cnt * sizeof(msink_edge_t)); - if (!gen) + msink_arr_init(); + + /* Sanity, nothing to do if aleady exists */ + if (cupsArrayFind(msink_arr, lookup_sink)) { - free(all); + free(lookup_sink); return; } - int gcnt = 0; - for (int i = 0; i < cnt; i++) + + /* Assign caller filetypes and add - copy callback will duplicate */ + lookup_sink->filetypes = filetypes; + cupsArrayAdd(msink_arr, lookup_sink); + + /* Verify insertion succeeded by finding the stored copy and log accordingly */ { - gen[gcnt] = all[i]; /* Copy the edge */ - - /* Normalize printer/asterix sources to printer/sink for signature calculation */ - if (!_cups_strcasecmp(all[i].super, "printer")) + msink_entry_t *found = (msink_entry_t *)cupsArrayFind(msink_arr, lookup_sink); + if (found) { - gen[gcnt].super = "printer"; - gen[gcnt].type = "sink"; + cupsdLogMessage(CUPSD_LOG_INFO, "sink-pattern-arr: store edges=%d supported=%d", + lookup_sink->edge_count, cupsArrayCount(found->filetypes)); } - - gcnt++; - } - qsort(gen, gcnt, sizeof(msink_edge_t), edge_cmp); - uint32_t sig = sig_hash(gen, gcnt); - unsigned bucket = (unsigned)(sig % MSINK_HT_SIZE); - - /* Check if this configuration already exists in the cache */ - msink_entry_t *ent; - for (ent = msink_table[bucket]; ent; ent = ent->next) - { - if (ent->edge_count == gcnt && edges_equal(ent->edges, gen, gcnt)) + else { - /* Already cached - nothing to do */ - free(gen); - free(all); - return; + cupsdLogMessage(CUPSD_LOG_ERROR, "sink-pattern-arr: failed to store edges=%d", + lookup_sink->edge_count); } } - - /* Create new cache entry */ - ent = (msink_entry_t *)calloc(1, sizeof(msink_entry_t)); - if (!ent) - { - free(gen); - free(all); - return; - } - ent->sig = sig; - ent->edge_count = gcnt; - ent->edges = (msink_edge_t *)malloc(gcnt * sizeof(msink_edge_t)); - if (!ent->edges) - { - free(gen); - free(all); - free(ent); - return; - } - memcpy(ent->edges, gen, gcnt * sizeof(msink_edge_t)); - free(gen); - free(all); - /* retain a copy of filetypes */ - ent->filetypes = cupsArrayNew(NULL, NULL); - if (ent->filetypes) - { - mime_type_t *mt; - for (mt = (mime_type_t *)cupsArrayFirst(filetypes); mt; mt = (mime_type_t *)cupsArrayNext(filetypes)) - cupsArrayAdd(ent->filetypes, mt); - } - ent->next = msink_table[bucket]; - msink_table[bucket] = ent; - cupsdLogMessage(CUPSD_LOG_INFO, "sink-pattern: store signature=%u edges=%d (printer/* normalized) supported=%d", sig, gcnt, cupsArrayCount(ent->filetypes)); - - /* Only log detailed edge info if debug level is high enough */ - if (LogLevel >= CUPSD_LOG_DEBUG) - { - for (int di = 0; di < gcnt; di++) - { - cupsdLogMessage(CUPSD_LOG_DEBUG, - "sink-pattern: edge[%d]: %s/%s cost=%d max=%zu prog_hash=%u", - di, ent->edges[di].super, ent->edges[di].type, ent->edges[di].cost, - ent->edges[di].maxsize, ent->edges[di].prog_hash); - } - } + lookup_sink->filetypes = NULL; + free(lookup_sink); } - /* - * 'msink_try_reuse()' - Try to reuse cached filetypes for a printer. + * msink_try_reuse() - public helper to attempt reusing sink filetypes */ - int msink_try_reuse(cupsd_printer_t *printer) { if (!printer) return 0; - if (!msink_is_enabled()) return 0; cups_array_t *reuse_filetypes = NULL; - - /* Try to find cached filetypes for this printer's sink configuration */ if (msink_reuse(MimeDatabase, printer->filetype, &reuse_filetypes)) { - /* Cache hit - set the filetypes, caller will handle IPP attributes */ printer->filetypes = reuse_filetypes; return 1; } - return 0; -} +} \ No newline at end of file