Coverage Report

Created: 2026-09-14 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-dict-extra/dict-fs.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "array.h"
5
#include "fs-api.h"
6
#include "istream.h"
7
#include "str.h"
8
#include "dict-transaction-memory.h"
9
#include "dict-private.h"
10
11
struct fs_dict {
12
  struct dict dict;
13
  struct fs *fs;
14
};
15
16
struct fs_dict_iterate_context {
17
  struct dict_iterate_context ctx;
18
  char *path;
19
  enum dict_iterate_flags flags;
20
  unsigned int key_count;
21
  pool_t value_pool;
22
  struct fs_iter *fs_iter;
23
  const char *const *values;
24
  char *error;
25
};
26
27
static int
28
fs_dict_init(const struct dict *dict_driver, struct event *event,
29
       struct dict **dict_r, const char **error_r)
30
0
{
31
0
  struct fs_parameters fs_param;
32
0
  struct fs *fs;
33
0
  struct fs_dict *dict;
34
35
0
  i_zero(&fs_param);
36
0
  if (fs_init_auto(event, &fs_param, &fs, error_r) <= 0)
37
0
    return -1;
38
39
0
  dict = i_new(struct fs_dict, 1);
40
0
  dict->dict = *dict_driver;
41
0
  dict->fs = fs;
42
43
0
  *dict_r = &dict->dict;
44
0
  return 0;
45
0
}
46
47
static void fs_dict_deinit(struct dict *_dict)
48
0
{
49
0
  struct fs_dict *dict = (struct fs_dict *)_dict;
50
51
0
  fs_deinit(&dict->fs);
52
0
  i_free(dict);
53
0
}
54
55
/* Remove unsafe paths */
56
static const char *fs_dict_escape_key(const char *key)
57
0
{
58
0
  const char *ptr;
59
0
  string_t *new_key = NULL;
60
  /* A key always starts with either "priv/" or "shared/". Usernames can
61
     start with "./" or "../". Thus make sure the given key has no such
62
     prefix. */
63
0
  if (str_begins_with(key, "./") || str_begins_with(key, "../")) {
64
0
    new_key = t_str_new(strlen(key));
65
0
    str_append(new_key, "..");
66
0
  }
67
  /* we take the slow path always if we see potential
68
     need for escaping */
69
0
  while ((ptr = strstr(key, "/.")) != NULL) {
70
    /* move to the first dot */
71
0
    const char *ptr2 = ptr + 1;
72
    /* find position of non-dot */
73
0
    while (*ptr2 == '.') ptr2++;
74
0
    if (new_key == NULL)
75
0
      new_key = t_str_new(strlen(key));
76
0
    str_append_data(new_key, key, ptr - key);
77
    /* if ptr2 is / or end of string, escape */
78
0
    if (*ptr2 == '/' || *ptr2 == '\0')
79
0
      str_append(new_key, "/...");
80
0
    else
81
0
      str_append(new_key, "/.");
82
0
    key = ptr + 2;
83
0
  }
84
0
  if (new_key == NULL)
85
0
    return key;
86
0
  str_append(new_key, key);
87
0
  return str_c(new_key);
88
0
}
89
90
static const char *fs_dict_get_full_key(const char *username, const char *key)
91
0
{
92
0
  key = fs_dict_escape_key(key);
93
0
  if (str_begins(key, DICT_PATH_SHARED, &key))
94
0
    return key;
95
0
  else if (str_begins(key, DICT_PATH_PRIVATE, &key))
96
0
    return t_strdup_printf("%s/%s", fs_dict_escape_key(username), key);
97
0
  else
98
0
    i_unreached();
99
0
}
100
101
static int fs_dict_lookup(struct dict *_dict, const struct dict_op_settings *set,
102
        pool_t pool, const char *key,
103
        const char *const **values_r, const char **error_r)
104
0
{
105
0
  struct fs_dict *dict = (struct fs_dict *)_dict;
106
0
  struct fs_file *file;
107
0
  struct istream *input;
108
0
  const unsigned char *data;
109
0
  size_t size;
110
0
  const char *path;
111
0
  string_t *str;
112
0
  int ret;
113
114
0
  path = fs_dict_get_full_key(set->username, key);
115
0
  file = fs_file_init(dict->fs, path, FS_OPEN_MODE_READONLY);
116
0
  input = fs_read_stream(file, IO_BLOCK_SIZE);
117
0
  (void)i_stream_read(input);
118
119
0
  str = str_new(pool, i_stream_get_data_size(input)+1);
120
0
  while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
121
0
    str_append_data(str, data, size);
122
0
    i_stream_skip(input, size);
123
0
  }
124
0
  i_assert(ret == -1);
125
126
0
  if (input->stream_errno == 0) {
127
0
    const char **values = p_new(pool, const char *, 2);
128
0
    values[0] = str_c(str);
129
0
    *values_r = values;
130
0
    ret = 1;
131
0
  } else {
132
0
    if (input->stream_errno == ENOENT)
133
0
      ret = 0;
134
0
    else {
135
0
      *error_r = t_strdup_printf("read(%s) failed: %s",
136
0
        path, i_stream_get_error(input));
137
0
    }
138
0
  }
139
140
0
  i_stream_unref(&input);
141
0
  fs_file_deinit(&file);
142
0
  return ret;
143
0
}
144
145
static struct dict_iterate_context *
146
fs_dict_iterate_init(struct dict *_dict, const struct dict_op_settings *set,
147
         const char *path, enum dict_iterate_flags flags)
148
0
{
149
0
  struct fs_dict *dict = (struct fs_dict *)_dict;
150
0
  struct fs_dict_iterate_context *iter;
151
152
0
  iter = i_new(struct fs_dict_iterate_context, 1);
153
0
  iter->ctx.dict = _dict;
154
0
  iter->path = i_strdup(path);
155
0
  iter->flags = flags;
156
0
  iter->value_pool = pool_alloconly_create("iterate value pool", 128);
157
0
  iter->fs_iter = fs_iter_init(dict->fs,
158
0
             fs_dict_get_full_key(set->username, path), 0);
159
160
0
  const char *unsupported = NULL;
161
0
  if ((flags & DICT_ITERATE_FLAG_RECURSE) != 0)
162
0
    unsupported = "DICT_ITERATE_FLAG_RECURSE";
163
0
  else if ((flags & DICT_ITERATE_FLAG_SORT_BY_KEY) != 0)
164
0
    unsupported = "DICT_ITERATE_FLAG_SORT_BY_KEY";
165
0
  else if ((flags & DICT_ITERATE_FLAG_SORT_BY_VALUE) != 0)
166
0
    unsupported = "DICT_ITERATE_FLAG_SORT_BY_VALUE";
167
0
  if (unsupported != NULL) {
168
0
    iter->error = i_strdup_printf(
169
0
      "dict-fs doesn't currently support %s", unsupported);
170
0
  }
171
0
  return &iter->ctx;
172
0
}
173
174
static bool fs_dict_iterate(struct dict_iterate_context *ctx,
175
          const char **key_r, const char *const **values_r)
176
0
{
177
0
  struct fs_dict_iterate_context *iter =
178
0
    (struct fs_dict_iterate_context *)ctx;
179
0
  const char *path, *error;
180
0
  int ret;
181
182
0
  if (iter->error != NULL || iter->fs_iter == NULL)
183
0
    return FALSE;
184
185
0
  *key_r = fs_iter_next(iter->fs_iter);
186
0
  if (*key_r == NULL) {
187
0
    if (iter->key_count > 0 ||
188
0
        (iter->flags & DICT_ITERATE_FLAG_EXACT_KEY) == 0)
189
0
      return FALSE;
190
191
0
    if (fs_iter_deinit(&iter->fs_iter, &error) == 0)
192
0
      return FALSE;
193
0
    if (errno != ENOTDIR) {
194
0
      iter->error = i_strdup(error);
195
0
      return FALSE;
196
0
    }
197
    /* the path itself is the only key we are going to return */
198
0
    *key_r = "";
199
0
  }
200
0
  iter->key_count++;
201
202
0
  p_clear(iter->value_pool);
203
0
  path = p_strconcat(iter->value_pool, iter->path, *key_r, NULL);
204
0
  if ((iter->flags & DICT_ITERATE_FLAG_NO_VALUE) != 0) {
205
0
    *key_r = path;
206
0
    return TRUE;
207
0
  }
208
0
  struct dict_op_settings set = {
209
0
    .username = ctx->set.username,
210
0
  };
211
0
  ret = fs_dict_lookup(ctx->dict, &set, iter->value_pool, path,
212
0
           &iter->values, &error);
213
0
  if (ret < 0) {
214
    /* I/O error */
215
0
    iter->error = i_strdup(error);
216
0
    return FALSE;
217
0
  } else if (ret == 0) {
218
    /* file was just deleted, just skip to next one */
219
0
    return fs_dict_iterate(ctx, key_r, values_r);
220
0
  }
221
0
  *key_r = path;
222
0
  *values_r = iter->values;
223
0
  return TRUE;
224
0
}
225
226
static int fs_dict_iterate_deinit(struct dict_iterate_context *ctx,
227
          const char **error_r)
228
0
{
229
0
  struct fs_dict_iterate_context *iter =
230
0
    (struct fs_dict_iterate_context *)ctx;
231
0
  const char *error;
232
0
  int ret;
233
234
0
  if (fs_iter_deinit(&iter->fs_iter, &error) < 0 && iter->error == NULL)
235
0
    iter->error = i_strdup(error);
236
237
0
  ret = iter->error != NULL ? -1 : 0;
238
0
  *error_r = t_strdup(iter->error);
239
240
0
  pool_unref(&iter->value_pool);
241
0
  i_free(iter->path);
242
0
  i_free(iter->error);
243
0
  i_free(iter);
244
0
  return ret;
245
0
}
246
247
static struct dict_transaction_context *
248
fs_dict_transaction_init(struct dict *_dict)
249
0
{
250
0
  struct dict_transaction_memory_context *ctx;
251
0
  pool_t pool;
252
253
0
  pool = pool_alloconly_create("file dict transaction", 2048);
254
0
  ctx = p_new(pool, struct dict_transaction_memory_context, 1);
255
0
  dict_transaction_memory_init(ctx, _dict, pool);
256
0
  return &ctx->ctx;
257
0
}
258
259
static int fs_dict_write_changes(struct dict_transaction_memory_context *ctx,
260
         const char **error_r)
261
0
{
262
0
  struct fs_dict *dict = (struct fs_dict *)ctx->ctx.dict;
263
0
  struct fs_file *file;
264
0
  const struct dict_transaction_memory_change *change;
265
0
  const char *key;
266
0
  int ret = 0;
267
268
0
  array_foreach(&ctx->changes, change) {
269
0
    key = fs_dict_get_full_key(ctx->ctx.set.username, change->key);
270
0
    switch (change->type) {
271
0
    case DICT_CHANGE_TYPE_SET:
272
0
      file = fs_file_init(dict->fs, key,
273
0
              FS_OPEN_MODE_REPLACE);
274
0
      if (fs_write(file, change->value.str, strlen(change->value.str)) < 0) {
275
0
        *error_r = t_strdup_printf(
276
0
          "fs_write(%s) failed: %s", key,
277
0
          fs_file_last_error(file));
278
0
        ret = -1;
279
0
      }
280
0
      fs_file_deinit(&file);
281
0
      break;
282
0
    case DICT_CHANGE_TYPE_UNSET:
283
0
      file = fs_file_init(dict->fs, key, FS_OPEN_MODE_READONLY);
284
0
      if (fs_delete(file) < 0) {
285
0
        *error_r = t_strdup_printf(
286
0
          "fs_delete(%s) failed: %s", key,
287
0
          fs_file_last_error(file));
288
0
        ret = -1;
289
0
      }
290
0
      fs_file_deinit(&file);
291
0
      break;
292
0
    case DICT_CHANGE_TYPE_INC:
293
0
      i_unreached();
294
0
    }
295
0
    if (ret < 0)
296
0
      return -1;
297
0
  }
298
0
  return 0;
299
0
}
300
301
static void
302
fs_dict_transaction_commit(struct dict_transaction_context *_ctx,
303
         bool async ATTR_UNUSED,
304
         dict_transaction_commit_callback_t *callback,
305
         void *context)
306
0
{
307
0
  struct dict_transaction_memory_context *ctx =
308
0
    (struct dict_transaction_memory_context *)_ctx;
309
0
  struct dict_commit_result result = { .ret = 1 };
310
311
312
0
  if (fs_dict_write_changes(ctx, &result.error) < 0)
313
0
    result.ret = -1;
314
0
  pool_unref(&ctx->pool);
315
316
0
  callback(&result, context);
317
0
}
318
319
struct dict dict_driver_fs = {
320
  .name = "fs",
321
  .v = {
322
    .init = fs_dict_init,
323
    .deinit = fs_dict_deinit,
324
    .lookup = fs_dict_lookup,
325
    .iterate_init = fs_dict_iterate_init,
326
    .iterate = fs_dict_iterate,
327
    .iterate_deinit = fs_dict_iterate_deinit,
328
    .transaction_init = fs_dict_transaction_init,
329
    .transaction_commit = fs_dict_transaction_commit,
330
    .transaction_rollback = dict_transaction_memory_rollback,
331
    .set = dict_transaction_memory_set,
332
    .unset = dict_transaction_memory_unset,
333
  }
334
};