2 * @brief Database factories for non-remote databases.
4 /* Copyright 2002,2003,2004,2005,2006,2007,2008,2009,2011,2012,2013,2014,2015,2016,2017 Olly Betts
5 * Copyright 2008 Lemur Consulting Ltd
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation; either version 2 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
25 #include "xapian/dbfactory.h"
27 #include "xapian/constants.h"
28 #include "xapian/database.h"
29 #include "xapian/error.h"
30 #include "xapian/version.h" // For XAPIAN_HAS_XXX_BACKEND.
33 #include "filetests.h"
34 #include "fileutils.h"
35 #include "posixy_wrapper.h"
38 #include "safeerrno.h"
39 #include <cstdlib> // For atoi().
41 #ifdef XAPIAN_HAS_GLASS_BACKEND
42 # include "glass/glass_database.h"
43 # include "glass/glass_defs.h"
45 #ifdef XAPIAN_HAS_INMEMORY_BACKEND
46 # include "inmemory/inmemory_database.h"
48 // Even if none of the above get included, we still need a definition of
49 // Database::Internal.
50 #include "backends/databaseinternal.h"
58 check_if_single_file_db(const struct stat
& sb
, const string
& path
,
61 #ifdef XAPIAN_HAS_GLASS_BACKEND
62 if (!S_ISREG(sb
.st_mode
)) return false;
63 // Look at the size as a clue - if it's 0 or not a multiple of
64 // GLASS_MIN_BLOCKSIZE, then it's not a single-file glass database. If it
65 // is, peek at the start of the file to determine which it is.
66 if (sb
.st_size
== 0 || sb
.st_size
% GLASS_MIN_BLOCKSIZE
!= 0)
68 int fd
= posixy_open(path
.c_str(), O_RDONLY
|O_BINARY
);
71 // FIXME: Don't duplicate magic check here...
72 if (io_read(fd
, magic_buf
, 14) == 14 &&
73 lseek(fd
, 0, SEEK_SET
) == 0 &&
74 memcmp(magic_buf
, "\x0f\x0dXapian Glass", 14) == 0) {
95 open_stub(Database
&db
, const string
&file
)
97 // A stub database is a text file with one or more lines of this format:
98 // <dbtype> <serialised db object>
100 // Lines which start with a "#" character are ignored.
102 // Any paths specified in stub database files which are relative will be
103 // considered to be relative to the directory containing the stub database.
104 ifstream
stub(file
.c_str());
106 string msg
= "Couldn't open stub database file: ";
108 throw Xapian::DatabaseOpeningError(msg
, errno
);
111 unsigned int line_no
= 0;
112 while (getline(stub
, line
)) {
114 if (line
.empty() || line
[0] == '#')
116 string::size_type space
= line
.find(' ');
117 if (space
== string::npos
) space
= line
.size();
119 string
type(line
, 0, space
);
120 line
.erase(0, space
+ 1);
122 if (type
== "auto") {
123 resolve_relative_path(line
, file
);
124 db
.add_database(Database(line
));
128 if (type
== "glass") {
129 #ifdef XAPIAN_HAS_GLASS_BACKEND
130 resolve_relative_path(line
, file
);
131 db
.add_database(Database(new GlassDatabase(line
)));
134 throw FeatureUnavailableError("Glass backend disabled");
138 if (type
== "remote" && !line
.empty()) {
139 #ifdef XAPIAN_HAS_REMOTE_BACKEND
140 if (line
[0] == ':') {
143 // Is it a security risk?
144 space
= line
.find(' ');
146 if (space
!= string::npos
) {
147 args
.assign(line
, space
+ 1, string::npos
);
148 line
.assign(line
, 1, space
- 1);
152 db
.add_database(Remote::open(line
, args
));
155 string::size_type colon
= line
.rfind(':');
156 if (colon
!= string::npos
) {
159 // Avoid misparsing an IPv6 address without a port number. The
160 // port number is required, so just leave that case to the
161 // error handling further below.
162 if (!(line
[0] == '[' && line
.back() == ']')) {
163 unsigned int port
= atoi(line
.c_str() + colon
+ 1);
165 if (line
[0] == '[' && line
.back() == ']') {
166 line
.erase(line
.size() - 1, 1);
169 db
.add_database(Remote::open(line
, port
));
174 throw FeatureUnavailableError("Remote backend disabled");
178 if (type
== "inmemory" && line
.empty()) {
179 #ifdef XAPIAN_HAS_INMEMORY_BACKEND
180 db
.add_database(Database(string(), DB_BACKEND_INMEMORY
));
183 throw FeatureUnavailableError("Inmemory backend disabled");
187 if (type
== "chert") {
188 throw FeatureUnavailableError("Chert backend no longer supported");
191 if (type
== "flint") {
192 throw FeatureUnavailableError("Flint backend no longer supported");
195 // Don't include the line itself - that might help an attacker
196 // by revealing part of a sensitive file's contents if they can
197 // arrange for it to be read as a stub database via infelicities in
198 // an application which uses Xapian. The line number is enough
199 // information to identify the problem line.
200 throw DatabaseOpeningError(file
+ ':' + str(line_no
) + ": Bad line");
203 // Allowing a stub database with no databases listed allows things like
204 // a "search all databases" feature to be implemented by generating a
205 // stub database file without having to special case there not being any
208 // 1.0.x throws DatabaseOpeningError here, but with a "Bad line" message
209 // with the line number just past the end of the file, which is a bit odd.
213 open_stub(WritableDatabase
&db
, const string
&file
, int flags
)
215 // A stub database is a text file with one or more lines of this format:
216 // <dbtype> <serialised db object>
218 // Lines which start with a "#" character, and lines which have no spaces
219 // in them, are ignored.
221 // Any paths specified in stub database files which are relative will be
222 // considered to be relative to the directory containing the stub database.
223 ifstream
stub(file
.c_str());
225 string msg
= "Couldn't open stub database file: ";
227 throw Xapian::DatabaseOpeningError(msg
, errno
);
230 unsigned int line_no
= 0;
232 if (!getline(stub
, line
)) break;
235 if (line
.empty() || line
[0] == '#')
237 string::size_type space
= line
.find(' ');
238 if (space
== string::npos
) space
= line
.size();
240 string
type(line
, 0, space
);
241 line
.erase(0, space
+ 1);
243 if (type
== "auto") {
244 resolve_relative_path(line
, file
);
245 db
.add_database(WritableDatabase(line
, flags
));
249 if (type
== "glass") {
250 #ifdef XAPIAN_HAS_GLASS_BACKEND
251 resolve_relative_path(line
, file
);
252 db
.add_database(WritableDatabase(line
, flags
|DB_BACKEND_GLASS
));
255 throw FeatureUnavailableError("Glass backend disabled");
259 if (type
== "remote" && !line
.empty()) {
260 #ifdef XAPIAN_HAS_REMOTE_BACKEND
261 if (line
[0] == ':') {
264 // Is it a security risk?
265 space
= line
.find(' ');
267 if (space
!= string::npos
) {
268 args
.assign(line
, space
+ 1, string::npos
);
269 line
.assign(line
, 1, space
- 1);
273 db
.add_database(Remote::open_writable(line
, args
, 0, flags
));
276 string::size_type colon
= line
.rfind(':');
277 if (colon
!= string::npos
) {
280 // Avoid misparsing an IPv6 address without a port number. The
281 // port number is required, so just leave that case to the
282 // error handling further below.
283 if (!(line
[0] == '[' && line
.back() == ']')) {
284 unsigned int port
= atoi(line
.c_str() + colon
+ 1);
286 if (line
[0] == '[' && line
.back() == ']') {
287 line
.erase(line
.size() - 1, 1);
290 db
.add_database(Remote::open_writable(line
, port
, 0, 10000, flags
));
295 throw FeatureUnavailableError("Remote backend disabled");
299 if (type
== "inmemory" && line
.empty()) {
300 #ifdef XAPIAN_HAS_INMEMORY_BACKEND
301 db
.add_database(WritableDatabase(string(), DB_BACKEND_INMEMORY
));
304 throw FeatureUnavailableError("Inmemory backend disabled");
308 if (type
== "chert") {
309 throw FeatureUnavailableError("Chert backend no longer supported");
312 if (type
== "flint") {
313 throw FeatureUnavailableError("Flint backend no longer supported");
316 // Don't include the line itself - that might help an attacker
317 // by revealing part of a sensitive file's contents if they can
318 // arrange for it to be read as a stub database via infelicities in
319 // an application which uses Xapian. The line number is enough
320 // information to identify the problem line.
321 throw DatabaseOpeningError(file
+ ':' + str(line_no
) + ": Bad line");
324 if (db
.internal
->size() == 0) {
325 throw DatabaseOpeningError(file
+ ": No databases listed");
329 Database::Database(const string
& path
, int flags
)
332 LOGCALL_CTOR(API
, "Database", path
|flags
);
334 int type
= flags
& DB_BACKEND_MASK_
;
336 case DB_BACKEND_CHERT
:
337 throw FeatureUnavailableError("Chert backend no longer supported");
338 case DB_BACKEND_GLASS
:
339 #ifdef XAPIAN_HAS_GLASS_BACKEND
340 internal
= new GlassDatabase(path
);
343 throw FeatureUnavailableError("Glass backend disabled");
345 case DB_BACKEND_STUB
:
346 open_stub(*this, path
);
348 case DB_BACKEND_INMEMORY
:
349 #ifdef XAPIAN_HAS_INMEMORY_BACKEND
350 internal
= new InMemoryDatabase();
353 throw FeatureUnavailableError("Inmemory backend disabled");
358 if (stat(path
.c_str(), &statbuf
) == -1) {
359 throw DatabaseOpeningError("Couldn't stat '" + path
+ "'", errno
);
362 if (S_ISREG(statbuf
.st_mode
)) {
363 // Could be a stub database file, or a single file glass database.
365 if (check_if_single_file_db(statbuf
, path
, &fd
)) {
366 #ifdef XAPIAN_HAS_GLASS_BACKEND
367 // Single file glass format.
368 internal
= new GlassDatabase(fd
);
371 throw FeatureUnavailableError("Glass backend disabled");
375 open_stub(*this, path
);
379 if (rare(!S_ISDIR(statbuf
.st_mode
))) {
380 throw DatabaseOpeningError("Not a regular file or directory: '" + path
+ "'");
383 #ifdef XAPIAN_HAS_GLASS_BACKEND
384 if (file_exists(path
+ "/iamglass")) {
385 internal
= new GlassDatabase(path
);
390 // Check for "stub directories".
391 string stub_file
= path
;
392 stub_file
+= "/XAPIANDB";
393 if (usual(file_exists(stub_file
))) {
394 open_stub(*this, stub_file
);
398 #ifndef XAPIAN_HAS_GLASS_BACKEND
399 if (file_exists(path
+ "/iamglass")) {
400 throw FeatureUnavailableError("Glass backend disabled");
403 if (file_exists(path
+ "/iamchert")) {
404 throw FeatureUnavailableError("Chert backend no longer supported");
406 if (file_exists(path
+ "/iamflint")) {
407 throw FeatureUnavailableError("Flint backend no longer supported");
410 throw DatabaseOpeningError("Couldn't detect type of database");
413 /** Helper factory function.
415 * This allows us to initialise Database::internal via the constructor's
416 * initialiser list, which we want to be able to do as Database::internal
417 * is an intrusive_ptr_nonnull, so we can't set it to NULL in the initialiser
418 * list and then fill it in later in the constructor body.
420 static Database::Internal
*
421 database_factory(int fd
, int flags
)
424 throw InvalidArgumentError("fd < 0");
426 #ifdef XAPIAN_HAS_GLASS_BACKEND
427 int type
= flags
& DB_BACKEND_MASK_
;
429 case 0: case DB_BACKEND_GLASS
:
430 return new GlassDatabase(fd
);
437 throw DatabaseOpeningError("Couldn't detect type of database");
440 Database::Database(int fd
, int flags
)
441 : internal(database_factory(fd
, flags
))
443 LOGCALL_CTOR(API
, "Database", fd
|flags
);
446 #if defined XAPIAN_HAS_GLASS_BACKEND
447 #define HAVE_DISK_BACKEND
450 WritableDatabase::WritableDatabase(const std::string
&path
, int flags
, int block_size
)
453 LOGCALL_CTOR(API
, "WritableDatabase", path
|flags
|block_size
);
454 // Avoid warning if all disk-based backends are disabled.
456 int type
= flags
& DB_BACKEND_MASK_
;
457 // Clear the backend bits, so we just pass on other flags to open_stub, etc.
458 flags
&= ~DB_BACKEND_MASK_
;
461 if (stat(path
.c_str(), &statbuf
) == -1) {
462 // ENOENT probably just means that we need to create the directory.
464 throw DatabaseOpeningError("Couldn't stat '" + path
+ "'", errno
);
466 // File or directory already exists.
468 if (S_ISREG(statbuf
.st_mode
)) {
469 // The path is a file, so assume it is a stub database file.
470 open_stub(*this, path
, flags
);
474 if (rare(!S_ISDIR(statbuf
.st_mode
))) {
475 throw DatabaseOpeningError("Not a regular file or directory: '" + path
+ "'");
478 if (file_exists(path
+ "/iamglass")) {
479 // Existing glass DB.
480 #ifdef XAPIAN_HAS_GLASS_BACKEND
481 type
= DB_BACKEND_GLASS
;
483 throw FeatureUnavailableError("Glass backend disabled");
485 } else if (file_exists(path
+ "/iamchert")) {
486 // Existing chert DB.
487 throw FeatureUnavailableError("Chert backend no longer supported");
488 } else if (file_exists(path
+ "/iamflint")) {
489 // Existing flint DB.
490 throw FeatureUnavailableError("Flint backend no longer supported");
492 // Check for "stub directories".
493 string stub_file
= path
;
494 stub_file
+= "/XAPIANDB";
495 if (usual(file_exists(stub_file
))) {
496 open_stub(*this, stub_file
, flags
);
504 case DB_BACKEND_STUB
:
505 open_stub(*this, path
, flags
);
508 // Fall through to first enabled case, so order the remaining cases
510 #ifdef XAPIAN_HAS_GLASS_BACKEND
511 case DB_BACKEND_GLASS
:
512 internal
= new GlassWritableDatabase(path
, flags
, block_size
);
515 case DB_BACKEND_CHERT
:
516 throw FeatureUnavailableError("Chert backend no longer supported");
517 case DB_BACKEND_INMEMORY
:
518 #ifdef XAPIAN_HAS_INMEMORY_BACKEND
519 internal
= new InMemoryDatabase();
522 throw FeatureUnavailableError("Inmemory backend disabled");
525 #ifndef HAVE_DISK_BACKEND
526 throw FeatureUnavailableError("No disk-based writable backend is enabled");