From 6beeb1b708550be0d4a53b272283e17e5e35fe17 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 17:01:30 +0200 Subject: Adding upstream version 2.4.57. Signed-off-by: Daniel Baumann --- modules/dav/fs/dbm.c | 800 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 800 insertions(+) create mode 100644 modules/dav/fs/dbm.c (limited to 'modules/dav/fs/dbm.c') diff --git a/modules/dav/fs/dbm.c b/modules/dav/fs/dbm.c new file mode 100644 index 0000000..347d75d --- /dev/null +++ b/modules/dav/fs/dbm.c @@ -0,0 +1,800 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* +** DAV extension module for Apache 2.0.* +** - Database support using DBM-style databases, +** part of the filesystem repository implementation +*/ + +/* +** This implementation uses a SDBM database per file and directory to +** record the properties. These databases are kept in a subdirectory (of +** the directory in question or the directory that holds the file in +** question) named by the macro DAV_FS_STATE_DIR (.DAV). The filename of the +** database is equivalent to the target filename, and is +** DAV_FS_STATE_FILE_FOR_DIR (.state_for_dir) for the directory itself. +*/ + +#include "apr_strings.h" +#include "apr_file_io.h" + +#include "apr_dbm.h" + +#define APR_WANT_BYTEFUNC +#include "apr_want.h" /* for ntohs and htons */ + +#include "apr_version.h" +#if !APR_VERSION_AT_LEAST(2,0,0) +#include "apu_version.h" +#endif + +#include "mod_dav.h" +#include "repos.h" +#include "http_log.h" +#include "http_main.h" /* for ap_server_conf */ + +APLOG_USE_MODULE(dav_fs); + +struct dav_db { + apr_pool_t *pool; + apr_dbm_t *file; + + /* when used as a property database: */ + + int version; /* *minor* version of this db */ + + dav_buffer ns_table; /* table of namespace URIs */ + short ns_count; /* number of entries in table */ + int ns_table_dirty; /* ns_table was modified */ + apr_hash_t *uri_index; /* map URIs to (1-based) table indices */ + + dav_buffer wb_key; /* work buffer for dav_gdbm_key */ + + apr_datum_t iter; /* iteration key */ +}; + +/* ------------------------------------------------------------------------- + * + * GENERIC DBM ACCESS + * + * For the most part, this just uses the APR DBM functions. They are wrapped + * a bit with some error handling (using the mod_dav error functions). + */ + +void dav_dbm_get_statefiles(apr_pool_t *p, const char *fname, + const char **state1, const char **state2) +{ + if (fname == NULL) + fname = DAV_FS_STATE_FILE_FOR_DIR; + + apr_dbm_get_usednames(p, fname, state1, state2); +} + +static dav_error * dav_fs_dbm_error(dav_db *db, apr_pool_t *p, + apr_status_t status) +{ + int errcode; + const char *errstr; + dav_error *err; + char errbuf[200]; + + if (status == APR_SUCCESS) + return NULL; + + p = db ? db->pool : p; + + /* There might not be a if we had problems creating it. */ + if (db == NULL) { + errcode = 1; + errstr = "Could not open property database."; + if (APR_STATUS_IS_EDSOOPEN(status)) + ap_log_error(APLOG_MARK, APLOG_CRIT, status, ap_server_conf, APLOGNO(00576) + "The DBM driver could not be loaded"); + } + else { + (void) apr_dbm_geterror(db->file, &errcode, errbuf, sizeof(errbuf)); + errstr = apr_pstrdup(p, errbuf); + } + + err = dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, errcode, status, errstr); + return err; +} + +/* ensure that our state subdirectory is present */ +/* ### does this belong here or in dav_fs_repos.c ?? */ +void dav_fs_ensure_state_dir(apr_pool_t * p, const char *dirname) +{ + const char *pathname = apr_pstrcat(p, dirname, "/" DAV_FS_STATE_DIR, NULL); + + /* ### do we need to deal with the umask? */ + + /* just try to make it, ignoring any resulting errors */ + (void) apr_dir_make(pathname, APR_OS_DEFAULT, p); +} + +/* dav_dbm_open_direct: Opens a *dbm database specified by path. + * ro = boolean read-only flag. + */ +dav_error * dav_dbm_open_direct(apr_pool_t *p, const char *pathname, int ro, + dav_db **pdb) +{ +#if APU_MAJOR_VERSION > 1 || (APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION >= 7) + const apr_dbm_driver_t *driver; + const apu_err_t *err; +#endif + apr_dbm_t *file = NULL; + apr_status_t status; + + *pdb = NULL; + +#if APU_MAJOR_VERSION > 1 || (APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION >= 7) + if ((status = apr_dbm_get_driver(&driver, NULL, &err, p)) != APR_SUCCESS) { + ap_log_error(APLOG_MARK, APLOG_ERR, status, ap_server_conf, APLOGNO(10289) + "mod_dav_fs: The DBM library '%s' could not be loaded: %s", + err->reason, err->msg); + return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 1, status, + "Could not load library for property database."); + } + if ((status = apr_dbm_open2(&file, driver, pathname, + ro ? APR_DBM_READONLY : APR_DBM_RWCREATE, + APR_OS_DEFAULT, p)) + != APR_SUCCESS && !ro) { + return dav_fs_dbm_error(NULL, p, status); + } +#else + if ((status = apr_dbm_open(&file, pathname, + ro ? APR_DBM_READONLY : APR_DBM_RWCREATE, + APR_OS_DEFAULT, p)) + != APR_SUCCESS + && !ro) { + /* ### do something with 'status' */ + + /* we can't continue if we couldn't open the file + and we need to write */ + return dav_fs_dbm_error(NULL, p, status); + } +#endif + + /* may be NULL if we tried to open a non-existent db as read-only */ + if (file != NULL) { + /* we have an open database... return it */ + *pdb = apr_pcalloc(p, sizeof(**pdb)); + (*pdb)->pool = p; + (*pdb)->file = file; + } + + return NULL; +} + +static dav_error * dav_dbm_open(apr_pool_t * p, const dav_resource *resource, + int ro, dav_db **pdb) +{ + const char *dirpath; + const char *fname; + const char *pathname; + + /* Get directory and filename for resource */ + /* ### should test this result value... */ + (void) dav_fs_dir_file_name(resource, &dirpath, &fname); + + /* If not opening read-only, ensure the state dir exists */ + if (!ro) { + /* ### what are the perf implications of always checking this? */ + dav_fs_ensure_state_dir(p, dirpath); + } + + pathname = apr_pstrcat(p, dirpath, "/" DAV_FS_STATE_DIR "/", + fname ? fname : DAV_FS_STATE_FILE_FOR_DIR, + NULL); + + /* ### readers cannot open while a writer has this open; we should + ### perform a few retries with random pauses. */ + + /* ### do we need to deal with the umask? */ + + return dav_dbm_open_direct(p, pathname, ro, pdb); +} + +void dav_dbm_close(dav_db *db) +{ + apr_dbm_close(db->file); +} + +dav_error * dav_dbm_fetch(dav_db *db, apr_datum_t key, apr_datum_t *pvalue) +{ + apr_status_t status; + + if (!key.dptr) { + /* no key could be created (namespace not known) => no value */ + memset(pvalue, 0, sizeof(*pvalue)); + status = APR_SUCCESS; + } else { + status = apr_dbm_fetch(db->file, key, pvalue); + } + + return dav_fs_dbm_error(db, NULL, status); +} + +dav_error * dav_dbm_store(dav_db *db, apr_datum_t key, apr_datum_t value) +{ + apr_status_t status = apr_dbm_store(db->file, key, value); + + return dav_fs_dbm_error(db, NULL, status); +} + +dav_error * dav_dbm_delete(dav_db *db, apr_datum_t key) +{ + apr_status_t status = apr_dbm_delete(db->file, key); + + return dav_fs_dbm_error(db, NULL, status); +} + +int dav_dbm_exists(dav_db *db, apr_datum_t key) +{ + return apr_dbm_exists(db->file, key); +} + +static dav_error * dav_dbm_firstkey(dav_db *db, apr_datum_t *pkey) +{ + apr_status_t status = apr_dbm_firstkey(db->file, pkey); + + return dav_fs_dbm_error(db, NULL, status); +} + +static dav_error * dav_dbm_nextkey(dav_db *db, apr_datum_t *pkey) +{ + apr_status_t status = apr_dbm_nextkey(db->file, pkey); + + return dav_fs_dbm_error(db, NULL, status); +} + +void dav_dbm_freedatum(dav_db *db, apr_datum_t data) +{ + apr_dbm_freedatum(db->file, data); +} + +/* ------------------------------------------------------------------------- + * + * PROPERTY DATABASE FUNCTIONS + */ + + +#define DAV_GDBM_NS_KEY "METADATA" +#define DAV_GDBM_NS_KEY_LEN 8 + +typedef struct { + unsigned char major; +#define DAV_DBVSN_MAJOR 4 + /* + ** V4 -- 0.9.9 .. + ** Prior versions could have keys or values with invalid + ** namespace prefixes as a result of the xmlns="" form not + ** resetting the default namespace to be "no namespace". The + ** namespace would be set to "" which is invalid; it should + ** be set to "no namespace". + ** + ** V3 -- 0.9.8 + ** Prior versions could have values with invalid namespace + ** prefixes due to an incorrect mapping of input to propdb + ** namespace indices. Version bumped to obsolete the old + ** values. + ** + ** V2 -- 0.9.7 + ** This introduced the xml:lang value into the property value's + ** record in the propdb. + ** + ** V1 -- .. 0.9.6 + ** Initial version. + */ + + + unsigned char minor; +#define DAV_DBVSN_MINOR 0 + + short ns_count; + +} dav_propdb_metadata; + +struct dav_deadprop_rollback { + apr_datum_t key; + apr_datum_t value; +}; + +struct dav_namespace_map { + int *ns_map; +}; + +/* +** Internal function to build a key +** +** WARNING: returns a pointer to a "static" buffer holding the key. The +** value must be copied or no longer used if this function is +** called again. +*/ +static apr_datum_t dav_build_key(dav_db *db, const dav_prop_name *name) +{ + char nsbuf[20]; + apr_size_t l_ns, l_name = strlen(name->name); + apr_datum_t key = { 0 }; + + /* + * Convert namespace ID to a string. "no namespace" is an empty string, + * so the keys will have the form ":name". Otherwise, the keys will + * have the form "#:name". + */ + if (*name->ns == '\0') { + nsbuf[0] = '\0'; + l_ns = 0; + } + else { + long ns_id = (long)apr_hash_get(db->uri_index, name->ns, + APR_HASH_KEY_STRING); + + + if (ns_id == 0) { + /* the namespace was not found(!) */ + return key; /* zeroed */ + } + + l_ns = apr_snprintf(nsbuf, sizeof(nsbuf), "%ld", ns_id - 1); + } + + /* assemble: #:name */ + dav_set_bufsize(db->pool, &db->wb_key, l_ns + 1 + l_name + 1); + memcpy(db->wb_key.buf, nsbuf, l_ns); + db->wb_key.buf[l_ns] = ':'; + memcpy(&db->wb_key.buf[l_ns + 1], name->name, l_name + 1); + + /* build the database key */ + key.dsize = l_ns + 1 + l_name + 1; + key.dptr = db->wb_key.buf; + + return key; +} + +static void dav_append_prop(apr_pool_t *pool, + const char *name, const char *value, + apr_text_header *phdr) +{ + const char *s; + const char *lang = value; + + /* skip past the xml:lang value */ + value += strlen(lang) + 1; + + if (*value == '\0') { + /* the property is an empty value */ + if (*name == ':') { + /* "no namespace" case */ + s = apr_pstrcat(pool, "<", name+1, "/>" DEBUG_CR, NULL); + } + else { + s = apr_pstrcat(pool, "" DEBUG_CR, NULL); + } + } + else if (*lang != '\0') { + if (*name == ':') { + /* "no namespace" case */ + s = apr_pstrcat(pool, "<", name+1, " xml:lang=\"", + lang, "\">", value, "" DEBUG_CR, + NULL); + } + else { + s = apr_pstrcat(pool, "", value, "" DEBUG_CR, + NULL); + } + } + else if (*name == ':') { + /* "no namespace" case */ + s = apr_pstrcat(pool, "<", name+1, ">", value, "" + DEBUG_CR, NULL); + } + else { + s = apr_pstrcat(pool, "", value, "" + DEBUG_CR, NULL); + } + + apr_text_append(pool, phdr, s); +} + +static dav_error * dav_propdb_open(apr_pool_t *pool, + const dav_resource *resource, int ro, + dav_db **pdb) +{ + dav_db *db; + dav_error *err; + apr_datum_t key; + apr_datum_t value = { 0 }; + + *pdb = NULL; + + /* + ** Return if an error occurred, or there is no database. + ** + ** NOTE: db could be NULL if we attempted to open a readonly + ** database that doesn't exist. If we require read/write + ** access, then a database was created and opened. + */ + if ((err = dav_dbm_open(pool, resource, ro, &db)) != NULL + || db == NULL) + return err; + + db->uri_index = apr_hash_make(pool); + + key.dptr = DAV_GDBM_NS_KEY; + key.dsize = DAV_GDBM_NS_KEY_LEN; + if ((err = dav_dbm_fetch(db, key, &value)) != NULL) { + /* ### push a higher-level description? */ + return err; + } + + if (value.dptr == NULL) { + dav_propdb_metadata m = { + DAV_DBVSN_MAJOR, DAV_DBVSN_MINOR, 0 + }; + + /* + ** If there is no METADATA key, then the database may be + ** from versions 0.9.0 .. 0.9.4 (which would be incompatible). + ** These can be identified by the presence of an NS_TABLE entry. + */ + key.dptr = "NS_TABLE"; + key.dsize = 8; + if (dav_dbm_exists(db, key)) { + dav_dbm_close(db); + + /* call it a major version error */ + return dav_new_error(pool, HTTP_INTERNAL_SERVER_ERROR, + DAV_ERR_PROP_BAD_MAJOR, 0, + "Prop database has the wrong major " + "version number and cannot be used."); + } + + /* initialize a new metadata structure */ + dav_set_bufsize(pool, &db->ns_table, sizeof(m)); + memcpy(db->ns_table.buf, &m, sizeof(m)); + } + else { + dav_propdb_metadata m; + long ns; + const char *uri; + + dav_set_bufsize(pool, &db->ns_table, value.dsize); + memcpy(db->ns_table.buf, value.dptr, value.dsize); + + memcpy(&m, value.dptr, sizeof(m)); + if (m.major != DAV_DBVSN_MAJOR) { + dav_dbm_close(db); + + return dav_new_error(pool, HTTP_INTERNAL_SERVER_ERROR, + DAV_ERR_PROP_BAD_MAJOR, 0, + "Prop database has the wrong major " + "version number and cannot be used."); + } + db->version = m.minor; + db->ns_count = ntohs(m.ns_count); + + dav_dbm_freedatum(db, value); + + /* create db->uri_index */ + for (ns = 0, uri = db->ns_table.buf + sizeof(dav_propdb_metadata); + ns++ < db->ns_count; + uri += strlen(uri) + 1) { + + /* we must copy the key, in case ns_table.buf moves */ + apr_hash_set(db->uri_index, + apr_pstrdup(pool, uri), APR_HASH_KEY_STRING, + (void *)ns); + } + } + + *pdb = db; + return NULL; +} + +static void dav_propdb_close(dav_db *db) +{ + + if (db->ns_table_dirty) { + dav_propdb_metadata m; + apr_datum_t key; + apr_datum_t value; + dav_error *err; + + key.dptr = DAV_GDBM_NS_KEY; + key.dsize = DAV_GDBM_NS_KEY_LEN; + + value.dptr = db->ns_table.buf; + value.dsize = db->ns_table.cur_len; + + /* fill in the metadata that we store into the prop db. */ + m.major = DAV_DBVSN_MAJOR; + m.minor = db->version; /* ### keep current minor version? */ + m.ns_count = htons(db->ns_count); + + memcpy(db->ns_table.buf, &m, sizeof(m)); + + err = dav_dbm_store(db, key, value); + if (err != NULL) + ap_log_error(APLOG_MARK, APLOG_WARNING, err->aprerr, ap_server_conf, + APLOGNO(00577) "Error writing propdb: %s", err->desc); + } + + dav_dbm_close(db); +} + +static dav_error * dav_propdb_define_namespaces(dav_db *db, dav_xmlns_info *xi) +{ + int ns; + const char *uri = db->ns_table.buf + sizeof(dav_propdb_metadata); + + /* within the prop values, we use "ns%d" for prefixes... register them */ + for (ns = 0; ns < db->ns_count; ++ns, uri += strlen(uri) + 1) { + + /* Empty URIs signify the empty namespace. These do not get a + namespace prefix. when we generate the value, we will simply + leave off the prefix, which is defined by mod_dav to be the + empty namespace. */ + if (*uri == '\0') + continue; + + /* ns_table.buf can move, so copy its value (we want the values to + last as long as the provided dav_xmlns_info). */ + dav_xmlns_add(xi, + apr_psprintf(xi->pool, "ns%d", ns), + apr_pstrdup(xi->pool, uri)); + } + + return NULL; +} + +static dav_error * dav_propdb_output_value(dav_db *db, + const dav_prop_name *name, + dav_xmlns_info *xi, + apr_text_header *phdr, + int *found) +{ + apr_datum_t key = dav_build_key(db, name); + apr_datum_t value; + dav_error *err; + + if ((err = dav_dbm_fetch(db, key, &value)) != NULL) + return err; + if (value.dptr == NULL) { + *found = 0; + return NULL; + } + *found = 1; + + dav_append_prop(db->pool, key.dptr, value.dptr, phdr); + + dav_dbm_freedatum(db, value); + + return NULL; +} + +static dav_error * dav_propdb_map_namespaces( + dav_db *db, + const apr_array_header_t *namespaces, + dav_namespace_map **mapping) +{ + dav_namespace_map *m = apr_palloc(db->pool, sizeof(*m)); + int i; + int *pmap; + const char **puri; + + /* + ** Iterate over the provided namespaces. If a namespace already appears + ** in our internal map of URI -> ns_id, then store that in the map. If + ** we don't know the namespace yet, then add it to the map and to our + ** table of known namespaces. + */ + m->ns_map = pmap = apr_palloc(db->pool, namespaces->nelts * sizeof(*pmap)); + for (i = namespaces->nelts, puri = (const char **)namespaces->elts; + i-- > 0; + ++puri, ++pmap) { + + const char *uri = *puri; + apr_size_t uri_len = strlen(uri); + long ns_id = (long)apr_hash_get(db->uri_index, uri, uri_len); + + if (ns_id == 0) { + dav_check_bufsize(db->pool, &db->ns_table, uri_len + 1); + memcpy(db->ns_table.buf + db->ns_table.cur_len, uri, uri_len + 1); + db->ns_table.cur_len += uri_len + 1; + + /* copy the uri in case the passed-in namespaces changes in + some way. */ + apr_hash_set(db->uri_index, apr_pstrdup(db->pool, uri), uri_len, + (void *)((long)(db->ns_count + 1))); + + db->ns_table_dirty = 1; + + *pmap = db->ns_count++; + } + else { + *pmap = ns_id - 1; + } + } + + *mapping = m; + return NULL; +} + +static dav_error * dav_propdb_store(dav_db *db, const dav_prop_name *name, + const apr_xml_elem *elem, + dav_namespace_map *mapping) +{ + apr_datum_t key = dav_build_key(db, name); + apr_datum_t value; + + /* Note: mapping->ns_map was set up in dav_propdb_map_namespaces() */ + + /* ### use a db- subpool for these values? clear on exit? */ + + /* quote all the values in the element */ + /* ### be nice to do this without affecting the element itself */ + /* ### of course, the cast indicates Badness is occurring here */ + apr_xml_quote_elem(db->pool, (apr_xml_elem *)elem); + + /* generate a text blob for the xml:lang plus the contents */ + apr_xml_to_text(db->pool, elem, APR_XML_X2T_LANG_INNER, NULL, + mapping->ns_map, + (const char **)&value.dptr, &value.dsize); + + return dav_dbm_store(db, key, value); +} + +static dav_error * dav_propdb_remove(dav_db *db, const dav_prop_name *name) +{ + apr_datum_t key = dav_build_key(db, name); + return dav_dbm_delete(db, key); +} + +static int dav_propdb_exists(dav_db *db, const dav_prop_name *name) +{ + apr_datum_t key = dav_build_key(db, name); + return dav_dbm_exists(db, key); +} + +static const char *dav_get_ns_table_uri(dav_db *db, int ns_id) +{ + const char *p = db->ns_table.buf + sizeof(dav_propdb_metadata); + + while (ns_id--) + p += strlen(p) + 1; + + return p; +} + +static void dav_set_name(dav_db *db, dav_prop_name *pname) +{ + const char *s = db->iter.dptr; + + if (s == NULL) { + pname->ns = pname->name = NULL; + } + else if (*s == ':') { + pname->ns = ""; + pname->name = s + 1; + } + else { + int id = atoi(s); + + pname->ns = dav_get_ns_table_uri(db, id); + if (s[1] == ':') { + pname->name = s + 2; + } + else { + pname->name = ap_strchr_c(s + 2, ':') + 1; + } + } +} + +static dav_error * dav_propdb_next_name(dav_db *db, dav_prop_name *pname) +{ + dav_error *err; + + /* free the previous key. note: if the loop is aborted, then the DBM + will toss the key (via pool cleanup) */ + if (db->iter.dptr != NULL) + dav_dbm_freedatum(db, db->iter); + + if ((err = dav_dbm_nextkey(db, &db->iter)) != NULL) + return err; + + /* skip past the METADATA key */ + if (db->iter.dptr != NULL && *db->iter.dptr == 'M') + return dav_propdb_next_name(db, pname); + + dav_set_name(db, pname); + return NULL; +} + +static dav_error * dav_propdb_first_name(dav_db *db, dav_prop_name *pname) +{ + dav_error *err; + + if ((err = dav_dbm_firstkey(db, &db->iter)) != NULL) + return err; + + /* skip past the METADATA key */ + if (db->iter.dptr != NULL && *db->iter.dptr == 'M') + return dav_propdb_next_name(db, pname); + + dav_set_name(db, pname); + return NULL; +} + +static dav_error * dav_propdb_get_rollback(dav_db *db, + const dav_prop_name *name, + dav_deadprop_rollback **prollback) +{ + dav_deadprop_rollback *rb = apr_pcalloc(db->pool, sizeof(*rb)); + apr_datum_t key; + apr_datum_t value; + dav_error *err; + + key = dav_build_key(db, name); + rb->key.dptr = apr_pstrdup(db->pool, key.dptr); + rb->key.dsize = key.dsize; + + if ((err = dav_dbm_fetch(db, key, &value)) != NULL) + return err; + if (value.dptr != NULL) { + rb->value.dptr = apr_pmemdup(db->pool, value.dptr, value.dsize); + rb->value.dsize = value.dsize; + } + + *prollback = rb; + return NULL; +} + +static dav_error * dav_propdb_apply_rollback(dav_db *db, + dav_deadprop_rollback *rollback) +{ + if (!rollback) { + return NULL; /* no rollback, nothing to do */ + } + + if (rollback->value.dptr == NULL) { + /* don't fail if the thing isn't really there. */ + (void) dav_dbm_delete(db, rollback->key); + return NULL; + } + + return dav_dbm_store(db, rollback->key, rollback->value); +} + +const dav_hooks_db dav_hooks_db_dbm = +{ + dav_propdb_open, + dav_propdb_close, + dav_propdb_define_namespaces, + dav_propdb_output_value, + dav_propdb_map_namespaces, + dav_propdb_store, + dav_propdb_remove, + dav_propdb_exists, + dav_propdb_first_name, + dav_propdb_next_name, + dav_propdb_get_rollback, + dav_propdb_apply_rollback, + + NULL /* ctx */ +}; -- cgit v1.2.3