]> git.sesse.net Git - plocate/blob - plocate-build.cpp
When reading mlocate.db, properly skip the configuration block.
[plocate] / plocate-build.cpp
1 #include "db.h"
2 #include "dprintf.h"
3 #include "turbopfor-encode.h"
4
5 #include <algorithm>
6 #include <assert.h>
7 #include <arpa/inet.h>
8 #include <chrono>
9 #include <getopt.h>
10 #include <iosfwd>
11 #include <math.h>
12 #include <memory>
13 #include <random>
14 #include <stdint.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <string>
19 #include <string_view>
20 #include <sys/stat.h>
21 #include <utility>
22 #include <vector>
23 #include <zdict.h>
24 #include <zstd.h>
25
26 #define P4NENC_BOUND(n) ((n + 127) / 128 + (n + 32) * sizeof(uint32_t))
27
28 #define NUM_TRIGRAMS 16777216
29
30 using namespace std;
31 using namespace std::chrono;
32
33 string zstd_compress(const string &src, ZSTD_CDict *cdict, string *tempbuf);
34
35 constexpr unsigned num_overflow_slots = 16;
36 bool use_debug = false;
37
38 static inline uint32_t read_unigram(const string_view s, size_t idx)
39 {
40         if (idx < s.size()) {
41                 return (unsigned char)s[idx];
42         } else {
43                 return 0;
44         }
45 }
46
47 static inline uint32_t read_trigram(const string_view s, size_t start)
48 {
49         return read_unigram(s, start) |
50                 (read_unigram(s, start + 1) << 8) |
51                 (read_unigram(s, start + 2) << 16);
52 }
53
54 enum {
55         DBE_NORMAL = 0, /* A non-directory file */
56         DBE_DIRECTORY = 1, /* A directory */
57         DBE_END = 2 /* End of directory contents; contains no name */
58 };
59
60 // From mlocate.
61 struct db_header {
62         uint8_t magic[8];
63         uint32_t conf_size;
64         uint8_t version;
65         uint8_t check_visibility;
66         uint8_t pad[2];
67 };
68
69 // From mlocate.
70 struct db_directory {
71         uint64_t time_sec;
72         uint32_t time_nsec;
73         uint8_t pad[4];
74 };
75
76 class PostingListBuilder {
77 public:
78         inline void add_docid(uint32_t docid);
79         void finish();
80
81         string encoded;
82         size_t num_docids = 0;
83
84 private:
85         void write_header(uint32_t docid);
86         void append_block();
87
88         vector<uint32_t> pending_deltas;
89
90         uint32_t last_block_end, last_docid = -1;
91 };
92
93 void PostingListBuilder::add_docid(uint32_t docid)
94 {
95         // Deduplicate against the last inserted value, if any.
96         if (docid == last_docid) {
97                 return;
98         }
99
100         if (num_docids == 0) {
101                 // Very first docid.
102                 write_header(docid);
103                 ++num_docids;
104                 last_block_end = last_docid = docid;
105                 return;
106         }
107
108         pending_deltas.push_back(docid - last_docid - 1);
109         last_docid = docid;
110         if (pending_deltas.size() == 128) {
111                 append_block();
112                 pending_deltas.clear();
113                 last_block_end = docid;
114         }
115         ++num_docids;
116 }
117
118 void PostingListBuilder::finish()
119 {
120         if (pending_deltas.empty()) {
121                 return;
122         }
123
124         assert(!encoded.empty());  // write_header() should already have run.
125
126         // No interleaving for partial blocks.
127         unsigned char buf[P4NENC_BOUND(128)];
128         unsigned char *end = encode_pfor_single_block<128>(pending_deltas.data(), pending_deltas.size(), /*interleaved=*/false, buf);
129         encoded.append(reinterpret_cast<char *>(buf), reinterpret_cast<char *>(end));
130 }
131
132 void PostingListBuilder::append_block()
133 {
134         unsigned char buf[P4NENC_BOUND(128)];
135         assert(pending_deltas.size() == 128);
136         unsigned char *end = encode_pfor_single_block<128>(pending_deltas.data(), 128, /*interleaved=*/true, buf);
137         encoded.append(reinterpret_cast<char *>(buf), reinterpret_cast<char *>(end));
138 }
139
140 void PostingListBuilder::write_header(uint32_t docid)
141 {
142         unsigned char buf[P4NENC_BOUND(1)];
143         unsigned char *end = write_baseval(docid, buf);
144         encoded.append(reinterpret_cast<char *>(buf), end - buf);
145 }
146
147 class DatabaseReceiver {
148 public:
149         virtual ~DatabaseReceiver() = default;
150         virtual void add_file(string filename) = 0;
151         virtual void flush_block() = 0;
152 };
153
154 class DictionaryBuilder : public DatabaseReceiver {
155 public:
156         DictionaryBuilder(size_t blocks_to_keep, size_t block_size)
157                 : blocks_to_keep(blocks_to_keep), block_size(block_size) {}
158         void add_file(string filename) override;
159         void flush_block() override;
160         string train(size_t buf_size);
161
162 private:
163         const size_t blocks_to_keep, block_size;
164         string current_block;
165         uint64_t block_num = 0;
166         size_t num_files_in_block = 0;
167
168         std::mt19937 reservoir_rand{ 1234 };  // Fixed seed for reproducibility.
169         bool keep_current_block = true;
170         int64_t slot_for_current_block = -1;
171
172         vector<string> sampled_blocks;
173         vector<size_t> lengths;
174 };
175
176 void DictionaryBuilder::add_file(string filename)
177 {
178         if (keep_current_block) {  // Only bother saving the filenames if we're actually keeping the block.
179                 if (!current_block.empty()) {
180                         current_block.push_back('\0');
181                 }
182                 current_block += filename;
183         }
184         if (++num_files_in_block == block_size) {
185                 flush_block();
186         }
187 }
188
189 void DictionaryBuilder::flush_block()
190 {
191         if (keep_current_block) {
192                 if (slot_for_current_block == -1) {
193                         lengths.push_back(current_block.size());
194                         sampled_blocks.push_back(move(current_block));
195                 } else {
196                         lengths[slot_for_current_block] = current_block.size();
197                         sampled_blocks[slot_for_current_block] = move(current_block);
198                 }
199         }
200         current_block.clear();
201         num_files_in_block = 0;
202         ++block_num;
203
204         if (block_num < blocks_to_keep) {
205                 keep_current_block = true;
206                 slot_for_current_block = -1;
207         } else {
208                 // Keep every block with equal probability (reservoir sampling).
209                 uint64_t idx = uniform_int_distribution<uint64_t>(0, block_num)(reservoir_rand);
210                 keep_current_block = (idx < blocks_to_keep);
211                 slot_for_current_block = idx;
212         }
213 }
214
215 string DictionaryBuilder::train(size_t buf_size)
216 {
217         string dictionary_buf;
218         sort(sampled_blocks.begin(), sampled_blocks.end());  // Seemingly important for decompression speed.
219         for (const string &block : sampled_blocks) {
220                 dictionary_buf += block;
221         }
222
223         string buf;
224         buf.resize(buf_size);
225         size_t ret = ZDICT_trainFromBuffer(&buf[0], buf_size, dictionary_buf.data(), lengths.data(), lengths.size());
226         if (ret == size_t(-1)) {
227                 return "";
228         }
229         dprintf("Sampled %zu bytes in %zu blocks, built a dictionary of size %zu\n", dictionary_buf.size(), lengths.size(), ret);
230         buf.resize(ret);
231
232         sampled_blocks.clear();
233         lengths.clear();
234
235         return buf;
236 }
237
238 class Corpus : public DatabaseReceiver {
239 public:
240         Corpus(FILE *outfp, size_t block_size, ZSTD_CDict *cdict)
241                 : invindex(new PostingListBuilder *[NUM_TRIGRAMS]), outfp(outfp), block_size(block_size), cdict(cdict)
242         {
243                 fill(invindex.get(), invindex.get() + NUM_TRIGRAMS, nullptr);
244         }
245         ~Corpus() override
246         {
247                 for (unsigned i = 0; i < NUM_TRIGRAMS; ++i) {
248                         delete invindex[i];
249                 }
250         }
251
252         void add_file(string filename) override;
253         void flush_block() override;
254
255         vector<uint64_t> filename_blocks;
256         size_t num_files = 0, num_files_in_block = 0, num_blocks = 0;
257         bool seen_trigram(uint32_t trgm)
258         {
259                 return invindex[trgm] != nullptr;
260         }
261         PostingListBuilder &get_pl_builder(uint32_t trgm)
262         {
263                 if (invindex[trgm] == nullptr) {
264                         invindex[trgm] = new PostingListBuilder;
265                 }
266                 return *invindex[trgm];
267         }
268         size_t num_trigrams() const;
269
270 private:
271         unique_ptr<PostingListBuilder *[]> invindex;
272         FILE *outfp;
273         string current_block;
274         string tempbuf;
275         const size_t block_size;
276         ZSTD_CDict *cdict;
277 };
278
279 void Corpus::add_file(string filename)
280 {
281         ++num_files;
282         if (!current_block.empty()) {
283                 current_block.push_back('\0');
284         }
285         current_block += filename;
286         if (++num_files_in_block == block_size) {
287                 flush_block();
288         }
289 }
290
291 void Corpus::flush_block()
292 {
293         if (current_block.empty()) {
294                 return;
295         }
296
297         uint32_t docid = num_blocks;
298
299         // Create trigrams.
300         const char *ptr = current_block.c_str();
301         while (ptr < current_block.c_str() + current_block.size()) {
302                 string_view s(ptr);
303                 if (s.size() >= 3) {
304                         for (size_t j = 0; j < s.size() - 2; ++j) {
305                                 uint32_t trgm = read_trigram(s, j);
306                                 get_pl_builder(trgm).add_docid(docid);
307                         }
308                 }
309                 ptr += s.size() + 1;
310         }
311
312         // Compress and add the filename block.
313         filename_blocks.push_back(ftell(outfp));
314         string compressed = zstd_compress(current_block, cdict, &tempbuf);
315         if (fwrite(compressed.data(), compressed.size(), 1, outfp) != 1) {
316                 perror("fwrite()");
317                 exit(1);
318         }
319
320         current_block.clear();
321         num_files_in_block = 0;
322         ++num_blocks;
323 }
324
325 size_t Corpus::num_trigrams() const
326 {
327         size_t num = 0;
328         for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) {
329                 if (invindex[trgm] != nullptr) {
330                         ++num;
331                 }
332         }
333         return num;
334 }
335
336 string read_cstr(FILE *fp)
337 {
338         string ret;
339         for (;;) {
340                 int ch = getc(fp);
341                 if (ch == -1) {
342                         perror("getc");
343                         exit(1);
344                 }
345                 if (ch == 0) {
346                         return ret;
347                 }
348                 ret.push_back(ch);
349         }
350 }
351
352 void handle_directory(FILE *fp, DatabaseReceiver *receiver)
353 {
354         db_directory dummy;
355         if (fread(&dummy, sizeof(dummy), 1, fp) != 1) {
356                 if (feof(fp)) {
357                         return;
358                 } else {
359                         perror("fread");
360                 }
361         }
362
363         string dir_path = read_cstr(fp);
364         if (dir_path == "/") {
365                 dir_path = "";
366         }
367
368         for (;;) {
369                 int type = getc(fp);
370                 if (type == DBE_NORMAL) {
371                         string filename = read_cstr(fp);
372                         receiver->add_file(dir_path + "/" + filename);
373                 } else if (type == DBE_DIRECTORY) {
374                         string dirname = read_cstr(fp);
375                         receiver->add_file(dir_path + "/" + dirname);
376                 } else {
377                         return;  // Probably end.
378                 }
379         }
380 }
381
382 void read_plaintext(FILE *fp, DatabaseReceiver *receiver)
383 {
384         if (fseek(fp, 0, SEEK_SET) != 0) {
385                 perror("fseek");
386                 exit(1);
387         }
388
389         while (!feof(fp)) {
390                 char buf[1024];
391                 if (fgets(buf, sizeof(buf), fp) == nullptr) {
392                         break;
393                 }
394                 string s(buf);
395                 assert(!s.empty());
396                 while (s.back() != '\n' && !feof(fp)) {
397                         // The string was longer than the buffer, so read again.
398                         if (fgets(buf, sizeof(buf), fp) == nullptr) {
399                                 break;
400                         }
401                         s += buf;
402                 }
403                 if (!s.empty() && s.back() == '\n')
404                         s.pop_back();
405                 receiver->add_file(move(s));
406         }
407 }
408
409 void read_mlocate(FILE *fp, DatabaseReceiver *receiver)
410 {
411         if (fseek(fp, 0, SEEK_SET) != 0) {
412                 perror("fseek");
413                 exit(1);
414         }
415
416         db_header hdr;
417         if (fread(&hdr, sizeof(hdr), 1, fp) != 1) {
418                 perror("short read");
419                 exit(1);
420         }
421
422         // TODO: Care about the base path.
423         string path = read_cstr(fp);
424
425         if (fseek(fp, ntohl(hdr.conf_size), SEEK_CUR) != 0) {
426                 perror("skip conf block");
427                 exit(1);
428         }
429
430         while (!feof(fp)) {
431                 handle_directory(fp, receiver);
432         }
433 }
434
435 string zstd_compress(const string &src, ZSTD_CDict *cdict, string *tempbuf)
436 {
437         static ZSTD_CCtx *ctx = nullptr;
438         if (ctx == nullptr) {
439                 ctx = ZSTD_createCCtx();
440         }
441
442         size_t max_size = ZSTD_compressBound(src.size());
443         if (tempbuf->size() < max_size) {
444                 tempbuf->resize(max_size);
445         }
446         size_t size;
447         if (cdict == nullptr) {
448                 size = ZSTD_compressCCtx(ctx, &(*tempbuf)[0], max_size, src.data(), src.size(), /*level=*/6);
449         } else {
450                 size = ZSTD_compress_usingCDict(ctx, &(*tempbuf)[0], max_size, src.data(), src.size(), cdict);
451         }
452         return string(tempbuf->data(), size);
453 }
454
455 bool is_prime(uint32_t x)
456 {
457         if ((x % 2) == 0 || (x % 3) == 0) {
458                 return false;
459         }
460         uint32_t limit = ceil(sqrt(x));
461         for (uint32_t factor = 5; factor <= limit; ++factor) {
462                 if ((x % factor) == 0) {
463                         return false;
464                 }
465         }
466         return true;
467 }
468
469 uint32_t next_prime(uint32_t x)
470 {
471         if ((x % 2) == 0) {
472                 ++x;
473         }
474         while (!is_prime(x)) {
475                 x += 2;
476         }
477         return x;
478 }
479
480 unique_ptr<Trigram[]> create_hashtable(Corpus &corpus, const vector<uint32_t> &all_trigrams, uint32_t ht_size, uint32_t num_overflow_slots)
481 {
482         unique_ptr<Trigram[]> ht(new Trigram[ht_size + num_overflow_slots + 1]);  // 1 for the sentinel element at the end.
483         for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) {
484                 ht[i].trgm = uint32_t(-1);
485                 ht[i].num_docids = 0;
486                 ht[i].offset = 0;
487         }
488         for (uint32_t trgm : all_trigrams) {
489                 // We don't know offset yet, so set it to zero.
490                 Trigram to_insert{ trgm, uint32_t(corpus.get_pl_builder(trgm).num_docids), 0 };
491
492                 uint32_t bucket = hash_trigram(trgm, ht_size);
493                 unsigned distance = 0;
494                 while (ht[bucket].num_docids != 0) {
495                         // Robin Hood hashing; reduces the longest distance by a lot.
496                         unsigned other_distance = bucket - hash_trigram(ht[bucket].trgm, ht_size);
497                         if (distance > other_distance) {
498                                 swap(to_insert, ht[bucket]);
499                                 distance = other_distance;
500                         }
501
502                         ++bucket, ++distance;
503                         if (distance > num_overflow_slots) {
504                                 return nullptr;
505                         }
506                 }
507                 ht[bucket] = to_insert;
508         }
509         return ht;
510 }
511
512 class DatabaseBuilder {
513 public:
514         DatabaseBuilder(const char *outfile, int block_size, string dictionary);
515         Corpus *start_corpus();
516         void finish_corpus();
517
518 private:
519         FILE *outfp;
520         Header hdr;
521         const int block_size;
522         steady_clock::time_point corpus_start;
523         Corpus *corpus = nullptr;
524         ZSTD_CDict *cdict = nullptr;
525 };
526
527 DatabaseBuilder::DatabaseBuilder(const char *outfile, int block_size, string dictionary)
528         : block_size(block_size)
529 {
530         umask(0027);
531         outfp = fopen(outfile, "wb");
532         if (outfp == nullptr) {
533                 perror(outfile);
534                 exit(1);
535         }
536
537         // Write the header.
538         memcpy(hdr.magic, "\0plocate", 8);
539         hdr.version = -1;  // Mark as broken.
540         hdr.hashtable_size = 0;  // Not known yet.
541         hdr.extra_ht_slots = num_overflow_slots;
542         hdr.num_docids = 0;
543         hdr.hash_table_offset_bytes = -1;  // We don't know these offsets yet.
544         hdr.max_version = 1;
545         hdr.filename_index_offset_bytes = -1;
546         hdr.zstd_dictionary_length_bytes = -1;
547         fwrite(&hdr, sizeof(hdr), 1, outfp);
548
549         if (dictionary.empty()) {
550                 hdr.zstd_dictionary_offset_bytes = 0;
551                 hdr.zstd_dictionary_length_bytes = 0;
552         } else {
553                 hdr.zstd_dictionary_offset_bytes = ftell(outfp);
554                 fwrite(dictionary.data(), dictionary.size(), 1, outfp);
555                 hdr.zstd_dictionary_length_bytes = dictionary.size();
556                 cdict = ZSTD_createCDict(dictionary.data(), dictionary.size(), /*level=*/6);
557         }
558 }
559
560 Corpus *DatabaseBuilder::start_corpus()
561 {
562         corpus_start = steady_clock::now();
563         corpus = new Corpus(outfp, block_size, cdict);
564         return corpus;
565 }
566
567 void DatabaseBuilder::finish_corpus()
568 {
569         corpus->flush_block();
570         hdr.num_docids = corpus->filename_blocks.size();
571
572         // Stick an empty block at the end as sentinel.
573         corpus->filename_blocks.push_back(ftell(outfp));
574         const size_t bytes_for_filenames = corpus->filename_blocks.back() - corpus->filename_blocks.front();
575
576         // Write the offsets to the filenames.
577         hdr.filename_index_offset_bytes = ftell(outfp);
578         const size_t bytes_for_filename_index = corpus->filename_blocks.size() * sizeof(uint64_t);
579         fwrite(corpus->filename_blocks.data(), corpus->filename_blocks.size(), sizeof(uint64_t), outfp);
580         corpus->filename_blocks.clear();
581         corpus->filename_blocks.shrink_to_fit();
582
583         // Finish up encoding the posting lists.
584         size_t trigrams = 0, longest_posting_list = 0;
585         size_t bytes_for_posting_lists = 0;
586         for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) {
587                 if (!corpus->seen_trigram(trgm))
588                         continue;
589                 PostingListBuilder &pl_builder = corpus->get_pl_builder(trgm);
590                 pl_builder.finish();
591                 longest_posting_list = max(longest_posting_list, pl_builder.num_docids);
592                 trigrams += pl_builder.num_docids;
593                 bytes_for_posting_lists += pl_builder.encoded.size();
594         }
595         size_t num_trigrams = corpus->num_trigrams();
596         dprintf("%zu files, %zu different trigrams, %zu entries, avg len %.2f, longest %zu\n",
597                 corpus->num_files, num_trigrams, trigrams, double(trigrams) / num_trigrams, longest_posting_list);
598         dprintf("%zu bytes used for posting lists (%.2f bits/entry)\n", bytes_for_posting_lists, 8 * bytes_for_posting_lists / double(trigrams));
599
600         dprintf("Building posting lists took %.1f ms.\n\n", 1e3 * duration<float>(steady_clock::now() - corpus_start).count());
601
602         // Find the used trigrams.
603         vector<uint32_t> all_trigrams;
604         for (unsigned trgm = 0; trgm < NUM_TRIGRAMS; ++trgm) {
605                 if (corpus->seen_trigram(trgm)) {
606                         all_trigrams.push_back(trgm);
607                 }
608         }
609
610         // Create the hash table.
611         unique_ptr<Trigram[]> hashtable;
612         uint32_t ht_size = next_prime(all_trigrams.size());
613         for (;;) {
614                 hashtable = create_hashtable(*corpus, all_trigrams, ht_size, num_overflow_slots);
615                 if (hashtable == nullptr) {
616                         dprintf("Failed creating hash table of size %u, increasing by 5%% and trying again.\n", ht_size);
617                         ht_size = next_prime(ht_size * 1.05);
618                 } else {
619                         dprintf("Created hash table of size %u.\n\n", ht_size);
620                         break;
621                 }
622         }
623
624         // Find the offsets for each posting list.
625         size_t bytes_for_hashtable = (ht_size + num_overflow_slots + 1) * sizeof(Trigram);
626         uint64_t offset = ftell(outfp) + bytes_for_hashtable;
627         for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) {
628                 hashtable[i].offset = offset;  // Needs to be there even for empty slots.
629                 if (hashtable[i].num_docids == 0) {
630                         continue;
631                 }
632
633                 const string &encoded = corpus->get_pl_builder(hashtable[i].trgm).encoded;
634                 offset += encoded.size();
635         }
636
637         // Write the hash table.
638         hdr.hash_table_offset_bytes = ftell(outfp);
639         hdr.hashtable_size = ht_size;
640         fwrite(hashtable.get(), ht_size + num_overflow_slots + 1, sizeof(Trigram), outfp);
641
642         // Write the actual posting lists.
643         for (unsigned i = 0; i < ht_size + num_overflow_slots + 1; ++i) {
644                 if (hashtable[i].num_docids == 0) {
645                         continue;
646                 }
647                 const string &encoded = corpus->get_pl_builder(hashtable[i].trgm).encoded;
648                 fwrite(encoded.data(), encoded.size(), 1, outfp);
649         }
650
651         // Rewind, and write the updated header.
652         hdr.version = 1;
653         fseek(outfp, 0, SEEK_SET);
654         fwrite(&hdr, sizeof(hdr), 1, outfp);
655         fclose(outfp);
656
657         size_t total_bytes = (bytes_for_hashtable + bytes_for_posting_lists + bytes_for_filename_index + bytes_for_filenames);
658
659         dprintf("Block size:     %7d files\n", block_size);
660         dprintf("Dictionary:     %'7.1f MB\n", hdr.zstd_dictionary_length_bytes / 1048576.0);
661         dprintf("Hash table:     %'7.1f MB\n", bytes_for_hashtable / 1048576.0);
662         dprintf("Posting lists:  %'7.1f MB\n", bytes_for_posting_lists / 1048576.0);
663         dprintf("Filename index: %'7.1f MB\n", bytes_for_filename_index / 1048576.0);
664         dprintf("Filenames:      %'7.1f MB\n", bytes_for_filenames / 1048576.0);
665         dprintf("Total:          %'7.1f MB\n", total_bytes / 1048576.0);
666         dprintf("\n");
667 }
668
669 void do_build(const char *infile, const char *outfile, int block_size, bool plaintext)
670 {
671         FILE *infp = fopen(infile, "rb");
672         if (infp == nullptr) {
673                 perror(infile);
674                 exit(1);
675         }
676
677         // Train the dictionary by sampling real blocks.
678         // The documentation for ZDICT_trainFromBuffer() claims that a reasonable
679         // dictionary size is ~100 kB, but 1 kB seems to actually compress better for us,
680         // and decompress just as fast.
681         DictionaryBuilder builder(/*blocks_to_keep=*/1000, block_size);
682         if (plaintext) {
683                 read_plaintext(infp, &builder);
684         } else {
685                 read_mlocate(infp, &builder);
686         }
687         string dictionary = builder.train(1024);
688
689         DatabaseBuilder db(outfile, block_size, dictionary);
690         Corpus *corpus = db.start_corpus();
691         if (plaintext) {
692                 read_plaintext(infp, corpus);
693         } else {
694                 read_mlocate(infp, corpus);
695         }
696         fclose(infp);
697
698         dprintf("Read %zu files from %s\n", corpus->num_files, infile);
699         db.finish_corpus();
700 }
701
702 void usage()
703 {
704         printf(
705                 "Usage: plocate-build MLOCATE_DB PLOCATE_DB\n"
706                 "\n"
707                 "Generate plocate index from mlocate.db, typically /var/lib/mlocate/mlocate.db.\n"
708                 "Normally, the destination should be /var/lib/mlocate/plocate.db.\n"
709                 "\n"
710                 "  -b, --block-size SIZE  number of filenames to store in each block (default 32)\n"
711                 "  -p, --plaintext        input is a plaintext file, not an mlocate database\n"
712                 "      --help             print this help\n"
713                 "      --version          print version information\n");
714 }
715
716 void version()
717 {
718         printf("plocate-build %s\n", PLOCATE_VERSION);
719         printf("Copyright 2020 Steinar H. Gunderson\n");
720         printf("License GPLv2+: GNU GPL version 2 or later <https://gnu.org/licenses/gpl.html>.\n");
721         printf("This is free software: you are free to change and redistribute it.\n");
722         printf("There is NO WARRANTY, to the extent permitted by law.\n");
723 }
724
725 int main(int argc, char **argv)
726 {
727         static const struct option long_options[] = {
728                 { "block-size", required_argument, 0, 'b' },
729                 { "plaintext", no_argument, 0, 'p' },
730                 { "help", no_argument, 0, 'h' },
731                 { "version", no_argument, 0, 'V' },
732                 { "debug", no_argument, 0, 'D' },  // Not documented.
733                 { 0, 0, 0, 0 }
734         };
735
736         int block_size = 32;
737         bool plaintext = false;
738
739         setlocale(LC_ALL, "");
740         for (;;) {
741                 int option_index = 0;
742                 int c = getopt_long(argc, argv, "b:hpVD", long_options, &option_index);
743                 if (c == -1) {
744                         break;
745                 }
746                 switch (c) {
747                 case 'b':
748                         block_size = atoi(optarg);
749                         break;
750                 case 'p':
751                         plaintext = true;
752                         break;
753                 case 'h':
754                         usage();
755                         exit(0);
756                 case 'V':
757                         version();
758                         exit(0);
759                 case 'D':
760                         use_debug = true;
761                         break;
762                 default:
763                         exit(1);
764                 }
765         }
766
767         if (argc - optind != 2) {
768                 usage();
769                 exit(1);
770         }
771
772         do_build(argv[optind], argv[optind + 1], block_size, plaintext);
773         exit(EXIT_SUCCESS);
774 }