1/* strings-table.c : operations on the `strings' table
2 *
3 * ====================================================================
4 *    Licensed to the Apache Software Foundation (ASF) under one
5 *    or more contributor license agreements.  See the NOTICE file
6 *    distributed with this work for additional information
7 *    regarding copyright ownership.  The ASF licenses this file
8 *    to you under the Apache License, Version 2.0 (the
9 *    "License"); you may not use this file except in compliance
10 *    with the License.  You may obtain a copy of the License at
11 *
12 *      http://www.apache.org/licenses/LICENSE-2.0
13 *
14 *    Unless required by applicable law or agreed to in writing,
15 *    software distributed under the License is distributed on an
16 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 *    KIND, either express or implied.  See the License for the
18 *    specific language governing permissions and limitations
19 *    under the License.
20 * ====================================================================
21 */
22
23#include "bdb_compat.h"
24#include "svn_fs.h"
25#include "svn_pools.h"
26#include "../fs.h"
27#include "../err.h"
28#include "dbt.h"
29#include "../trail.h"
30#include "../key-gen.h"
31#include "../../libsvn_fs/fs-loader.h"
32#include "bdb-err.h"
33#include "strings-table.h"
34
35#include "svn_private_config.h"
36
37
38/*** Creating and opening the strings table. ***/
39
40int
41svn_fs_bdb__open_strings_table(DB **strings_p,
42                               DB_ENV *env,
43                               svn_boolean_t create)
44{
45  const u_int32_t open_flags = (create ? (DB_CREATE | DB_EXCL) : 0);
46  DB *strings;
47
48  BDB_ERR(svn_fs_bdb__check_version());
49  BDB_ERR(db_create(&strings, env, 0));
50
51  /* Enable duplicate keys. This allows the data to be spread out across
52     multiple records. Note: this must occur before ->open().  */
53  BDB_ERR(strings->set_flags(strings, DB_DUP));
54
55  BDB_ERR((strings->open)(SVN_BDB_OPEN_PARAMS(strings, NULL),
56                          "strings", 0, DB_BTREE,
57                          open_flags, 0666));
58
59  if (create)
60    {
61      DBT key, value;
62
63      /* Create the `next-key' table entry.  */
64      BDB_ERR(strings->put
65              (strings, 0,
66               svn_fs_base__str_to_dbt(&key, NEXT_KEY_KEY),
67               svn_fs_base__str_to_dbt(&value, "0"), 0));
68    }
69
70  *strings_p = strings;
71  return 0;
72}
73
74
75
76/*** Storing and retrieving strings.  ***/
77
78/* Allocate *CURSOR and advance it to first row in the set of rows
79   whose key is defined by QUERY.  Set *LENGTH to the size of that
80   first row.  */
81static svn_error_t *
82locate_key(apr_size_t *length,
83           DBC **cursor,
84           DBT *query,
85           svn_fs_t *fs,
86           trail_t *trail,
87           apr_pool_t *pool)
88{
89  base_fs_data_t *bfd = fs->fsap_data;
90  int db_err;
91  DBT result;
92
93  svn_fs_base__trail_debug(trail, "strings", "cursor");
94  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
95                   bfd->strings->cursor(bfd->strings, trail->db_txn,
96                                        cursor, 0)));
97
98  /* Set up the DBT for reading the length of the record. */
99  svn_fs_base__clear_dbt(&result);
100  result.ulen = 0;
101  result.flags |= DB_DBT_USERMEM;
102
103  /* Advance the cursor to the key that we're looking for. */
104  db_err = svn_bdb_dbc_get(*cursor, query, &result, DB_SET);
105
106  /* We don't need to svn_fs_base__track_dbt() the result, because nothing
107     was allocated in it. */
108
109  /* If there's no such node, return an appropriately specific error.  */
110  if (db_err == DB_NOTFOUND)
111    {
112      svn_bdb_dbc_close(*cursor);
113      return svn_error_createf
114        (SVN_ERR_FS_NO_SUCH_STRING, 0,
115         "No such string '%s'", (const char *)query->data);
116    }
117  if (db_err)
118    {
119      DBT rerun;
120
121      if (db_err != SVN_BDB_DB_BUFFER_SMALL)
122        {
123          svn_bdb_dbc_close(*cursor);
124          return BDB_WRAP(fs, N_("moving cursor"), db_err);
125        }
126
127      /* We got an SVN_BDB_DB_BUFFER_SMALL (typical since we have a
128         zero length buf), so we need to re-run the operation to make
129         it happen. */
130      svn_fs_base__clear_dbt(&rerun);
131      rerun.flags |= DB_DBT_USERMEM | DB_DBT_PARTIAL;
132      db_err = svn_bdb_dbc_get(*cursor, query, &rerun, DB_SET);
133      if (db_err)
134        {
135          svn_bdb_dbc_close(*cursor);
136          return BDB_WRAP(fs, N_("rerunning cursor move"), db_err);
137        }
138    }
139
140  /* ### this cast might not be safe? */
141  *length = (apr_size_t) result.size;
142
143  return SVN_NO_ERROR;
144}
145
146
147/* Advance CURSOR by a single row in the set of rows whose keys match
148   CURSOR's current location.  Set *LENGTH to the size of that next
149   row.  If any error occurs, CURSOR will be destroyed.  */
150static int
151get_next_length(apr_size_t *length, DBC *cursor, DBT *query)
152{
153  DBT result;
154  int db_err;
155
156  /* Set up the DBT for reading the length of the record. */
157  svn_fs_base__clear_dbt(&result);
158  result.ulen = 0;
159  result.flags |= DB_DBT_USERMEM;
160
161  /* Note: this may change the QUERY DBT, but that's okay: we're going
162     to be sticking with the same key anyways.  */
163  db_err = svn_bdb_dbc_get(cursor, query, &result, DB_NEXT_DUP);
164
165  /* Note that we exit on DB_NOTFOUND. The caller uses that to end a loop. */
166  if (db_err)
167    {
168      DBT rerun;
169
170      if (db_err != SVN_BDB_DB_BUFFER_SMALL)
171        {
172          svn_bdb_dbc_close(cursor);
173          return db_err;
174        }
175
176      /* We got an SVN_BDB_DB_BUFFER_SMALL (typical since we have a
177         zero length buf), so we need to re-run the operation to make
178         it happen. */
179      svn_fs_base__clear_dbt(&rerun);
180      rerun.flags |= DB_DBT_USERMEM | DB_DBT_PARTIAL;
181      db_err = svn_bdb_dbc_get(cursor, query, &rerun, DB_NEXT_DUP);
182      if (db_err)
183        svn_bdb_dbc_close(cursor);
184    }
185
186  /* ### this cast might not be safe? */
187  *length = (apr_size_t) result.size;
188  return db_err;
189}
190
191
192svn_error_t *
193svn_fs_bdb__string_read(svn_fs_t *fs,
194                        const char *key,
195                        char *buf,
196                        svn_filesize_t offset,
197                        apr_size_t *len,
198                        trail_t *trail,
199                        apr_pool_t *pool)
200{
201  int db_err;
202  DBT query, result;
203  DBC *cursor;
204  apr_size_t length, bytes_read = 0;
205
206  svn_fs_base__str_to_dbt(&query, key);
207
208  SVN_ERR(locate_key(&length, &cursor, &query, fs, trail, pool));
209
210  /* Seek through the records for this key, trying to find the record that
211     includes OFFSET. Note that we don't require reading from more than
212     one record since we're allowed to return partial reads.  */
213  while (length <= offset)
214    {
215      offset -= length;
216
217      /* Remember, if any error happens, our cursor has been closed
218         for us. */
219      db_err = get_next_length(&length, cursor, &query);
220
221      /* No more records? They tried to read past the end. */
222      if (db_err == DB_NOTFOUND)
223        {
224          *len = 0;
225          return SVN_NO_ERROR;
226        }
227      if (db_err)
228        return BDB_WRAP(fs, N_("reading string"), db_err);
229    }
230
231  /* The current record contains OFFSET. Fetch the contents now. Note that
232     OFFSET has been moved to be relative to this record. The length could
233     quite easily extend past this record, so we use DB_DBT_PARTIAL and
234     read successive records until we've filled the request.  */
235  while (1)
236    {
237      svn_fs_base__clear_dbt(&result);
238      result.data = buf + bytes_read;
239      result.ulen = (u_int32_t)(*len - bytes_read);
240      result.doff = (u_int32_t)offset;
241      result.dlen = result.ulen;
242      result.flags |= (DB_DBT_USERMEM | DB_DBT_PARTIAL);
243      db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_CURRENT);
244      if (db_err)
245        {
246          svn_bdb_dbc_close(cursor);
247          return BDB_WRAP(fs, N_("reading string"), db_err);
248        }
249
250      bytes_read += result.size;
251      if (bytes_read == *len)
252        {
253          /* Done with the cursor. */
254          SVN_ERR(BDB_WRAP(fs, N_("closing string-reading cursor"),
255                           svn_bdb_dbc_close(cursor)));
256          break;
257        }
258
259      /* Remember, if any error happens, our cursor has been closed
260         for us. */
261      db_err = get_next_length(&length, cursor, &query);
262      if (db_err == DB_NOTFOUND)
263        break;
264      if (db_err)
265        return BDB_WRAP(fs, N_("reading string"), db_err);
266
267      /* We'll be reading from the beginning of the next record */
268      offset = 0;
269    }
270
271  *len = bytes_read;
272  return SVN_NO_ERROR;
273}
274
275
276/* Get the current 'next-key' value and bump the record. */
277static svn_error_t *
278get_key_and_bump(svn_fs_t *fs,
279                 const char **key,
280                 trail_t *trail,
281                 apr_pool_t *pool)
282{
283  base_fs_data_t *bfd = fs->fsap_data;
284  DBC *cursor;
285  char next_key[MAX_KEY_SIZE];
286  apr_size_t key_len;
287  int db_err;
288  DBT query;
289  DBT result;
290
291  /* ### todo: see issue #409 for why bumping the key as part of this
292     trail is problematic. */
293
294  /* Open a cursor and move it to the 'next-key' value. We can then fetch
295     the contents and use the cursor to overwrite those contents. Since
296     this database allows duplicates, we can't do an arbitrary 'put' to
297     write the new value -- that would append, not overwrite.  */
298
299  svn_fs_base__trail_debug(trail, "strings", "cursor");
300  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
301                   bfd->strings->cursor(bfd->strings, trail->db_txn,
302                                        &cursor, 0)));
303
304  /* Advance the cursor to 'next-key' and read it. */
305
306  db_err = svn_bdb_dbc_get(cursor,
307                           svn_fs_base__str_to_dbt(&query, NEXT_KEY_KEY),
308                           svn_fs_base__result_dbt(&result),
309                           DB_SET);
310  if (db_err)
311    {
312      svn_bdb_dbc_close(cursor);
313      return BDB_WRAP(fs, N_("getting next-key value"), db_err);
314    }
315
316  svn_fs_base__track_dbt(&result, pool);
317  *key = apr_pstrmemdup(pool, result.data, result.size);
318
319  /* Bump to future key. */
320  key_len = result.size;
321  svn_fs_base__next_key(result.data, &key_len, next_key);
322
323  /* Shove the new key back into the database, at the cursor position. */
324  db_err = svn_bdb_dbc_put(cursor, &query,
325                           svn_fs_base__str_to_dbt(&result, next_key),
326                           DB_CURRENT);
327  if (db_err)
328    {
329      svn_bdb_dbc_close(cursor); /* ignore the error, the original is
330                                    more important. */
331      return BDB_WRAP(fs, N_("bumping next string key"), db_err);
332    }
333
334  return BDB_WRAP(fs, N_("closing string-reading cursor"),
335                  svn_bdb_dbc_close(cursor));
336}
337
338svn_error_t *
339svn_fs_bdb__string_append(svn_fs_t *fs,
340                          const char **key,
341                          apr_size_t len,
342                          const char *buf,
343                          trail_t *trail,
344                          apr_pool_t *pool)
345{
346  base_fs_data_t *bfd = fs->fsap_data;
347  DBT query, result;
348
349  /* If the passed-in key is NULL, we graciously generate a new string
350     using the value of the `next-key' record in the strings table. */
351  if (*key == NULL)
352    {
353      SVN_ERR(get_key_and_bump(fs, key, trail, pool));
354    }
355
356  /* Store a new record into the database. */
357  svn_fs_base__trail_debug(trail, "strings", "put");
358  return BDB_WRAP(fs, N_("appending string"),
359                  bfd->strings->put
360                  (bfd->strings, trail->db_txn,
361                   svn_fs_base__str_to_dbt(&query, *key),
362                   svn_fs_base__set_dbt(&result, buf, len),
363                   0));
364}
365
366
367svn_error_t *
368svn_fs_bdb__string_clear(svn_fs_t *fs,
369                         const char *key,
370                         trail_t *trail,
371                         apr_pool_t *pool)
372{
373  base_fs_data_t *bfd = fs->fsap_data;
374  int db_err;
375  DBT query, result;
376
377  svn_fs_base__str_to_dbt(&query, key);
378
379  /* Torch the prior contents */
380  svn_fs_base__trail_debug(trail, "strings", "del");
381  db_err = bfd->strings->del(bfd->strings, trail->db_txn, &query, 0);
382
383  /* If there's no such node, return an appropriately specific error.  */
384  if (db_err == DB_NOTFOUND)
385    return svn_error_createf
386      (SVN_ERR_FS_NO_SUCH_STRING, 0,
387       "No such string '%s'", key);
388
389  /* Handle any other error conditions.  */
390  SVN_ERR(BDB_WRAP(fs, N_("clearing string"), db_err));
391
392  /* Shove empty data back in for this key. */
393  svn_fs_base__clear_dbt(&result);
394  result.data = 0;
395  result.size = 0;
396  result.flags |= DB_DBT_USERMEM;
397
398  svn_fs_base__trail_debug(trail, "strings", "put");
399  return BDB_WRAP(fs, N_("storing empty contents"),
400                  bfd->strings->put(bfd->strings, trail->db_txn,
401                                    &query, &result, 0));
402}
403
404
405svn_error_t *
406svn_fs_bdb__string_size(svn_filesize_t *size,
407                        svn_fs_t *fs,
408                        const char *key,
409                        trail_t *trail,
410                        apr_pool_t *pool)
411{
412  int db_err;
413  DBT query;
414  DBC *cursor;
415  apr_size_t length;
416  svn_filesize_t total;
417
418  svn_fs_base__str_to_dbt(&query, key);
419
420  SVN_ERR(locate_key(&length, &cursor, &query, fs, trail, pool));
421
422  total = length;
423  while (1)
424    {
425      /* Remember, if any error happens, our cursor has been closed
426         for us. */
427      db_err = get_next_length(&length, cursor, &query);
428
429      /* No more records? Then return the total length. */
430      if (db_err == DB_NOTFOUND)
431        {
432          *size = total;
433          return SVN_NO_ERROR;
434        }
435      if (db_err)
436        return BDB_WRAP(fs, N_("fetching string length"), db_err);
437
438      total += length;
439    }
440
441  /* NOTREACHED */
442}
443
444
445svn_error_t *
446svn_fs_bdb__string_delete(svn_fs_t *fs,
447                          const char *key,
448                          trail_t *trail,
449                          apr_pool_t *pool)
450{
451  base_fs_data_t *bfd = fs->fsap_data;
452  int db_err;
453  DBT query;
454
455  svn_fs_base__trail_debug(trail, "strings", "del");
456  db_err = bfd->strings->del(bfd->strings, trail->db_txn,
457                             svn_fs_base__str_to_dbt(&query, key), 0);
458
459  /* If there's no such node, return an appropriately specific error.  */
460  if (db_err == DB_NOTFOUND)
461    return svn_error_createf
462      (SVN_ERR_FS_NO_SUCH_STRING, 0,
463       "No such string '%s'", key);
464
465  /* Handle any other error conditions.  */
466  return BDB_WRAP(fs, N_("deleting string"), db_err);
467}
468
469
470svn_error_t *
471svn_fs_bdb__string_copy(svn_fs_t *fs,
472                        const char **new_key,
473                        const char *key,
474                        trail_t *trail,
475                        apr_pool_t *pool)
476{
477  base_fs_data_t *bfd = fs->fsap_data;
478  DBT query;
479  DBT result;
480  DBT copykey;
481  DBC *cursor;
482  int db_err;
483
484  /* Copy off the old key in case the caller is sharing storage
485     between the old and new keys. */
486  const char *old_key = apr_pstrdup(pool, key);
487
488  SVN_ERR(get_key_and_bump(fs, new_key, trail, pool));
489
490  svn_fs_base__trail_debug(trail, "strings", "cursor");
491  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
492                   bfd->strings->cursor(bfd->strings, trail->db_txn,
493                                        &cursor, 0)));
494
495  svn_fs_base__str_to_dbt(&query, old_key);
496  svn_fs_base__str_to_dbt(&copykey, *new_key);
497
498  svn_fs_base__clear_dbt(&result);
499
500  /* Move to the first record and fetch its data (under BDB's mem mgmt). */
501  db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_SET);
502  if (db_err)
503    {
504      svn_bdb_dbc_close(cursor);
505      return BDB_WRAP(fs, N_("getting next-key value"), db_err);
506    }
507
508  while (1)
509    {
510      /* ### can we pass a BDB-provided buffer to another BDB function?
511         ### they are supposed to have a duration up to certain points
512         ### of calling back into BDB, but I'm not sure what the exact
513         ### rules are. it is definitely nicer to use BDB buffers here
514         ### to simplify things and reduce copies, but... hrm.
515      */
516
517      /* Write the data to the database */
518      svn_fs_base__trail_debug(trail, "strings", "put");
519      db_err = bfd->strings->put(bfd->strings, trail->db_txn,
520                                 &copykey, &result, 0);
521      if (db_err)
522        {
523          svn_bdb_dbc_close(cursor);
524          return BDB_WRAP(fs, N_("writing copied data"), db_err);
525        }
526
527      /* Read the next chunk. Terminate loop if we're done. */
528      svn_fs_base__clear_dbt(&result);
529      db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_NEXT_DUP);
530      if (db_err == DB_NOTFOUND)
531        break;
532      if (db_err)
533        {
534          svn_bdb_dbc_close(cursor);
535          return BDB_WRAP(fs, N_("fetching string data for a copy"), db_err);
536        }
537    }
538
539  return BDB_WRAP(fs, N_("closing string-reading cursor"),
540                  svn_bdb_dbc_close(cursor));
541}
542