X-Git-Url: https://git.sesse.net/?p=plocate;a=blobdiff_plain;f=plocate.cpp;h=9606d2f49db1892be4a145e752262bffaac77e0b;hp=390bd21e681dcc7ad6d77458e599bb72c5424e17;hb=6dd0338abec6c820f75d21d441b0a60e3427539f;hpb=e3a362166c8012ebc689b437b5ead928c11fbe3e diff --git a/plocate.cpp b/plocate.cpp index 390bd21..9606d2f 100644 --- a/plocate.cpp +++ b/plocate.cpp @@ -1,112 +1,456 @@ +#include "access_rx_cache.h" +#include "complete_pread.h" +#include "db.h" +#include "dprintf.h" +#include "io_uring_engine.h" +#include "needle.h" +#include "parse_trigrams.h" +#include "serializer.h" +#include "turbopfor.h" +#include "unique_sort.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include -#include -#include #include -#include -#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include +#include +#include +#include +#include #include -#include "vp4.h" - -#define P4NENC_BOUND(n) ((n+127)/128+(n+32)*sizeof(uint32_t)) - using namespace std; using namespace std::chrono; -#define dprintf(...) -//#define dprintf(...) fprintf(stderr, __VA_ARGS__); - -static inline uint32_t read_unigram(const string &s, size_t idx) +bool ignore_case = false; +bool only_count = false; +bool print_nul = false; +bool use_debug = false; +bool flush_cache = false; +bool patterns_are_regex = false; +bool use_extended_regex = false; +bool match_basename = false; +int64_t limit_matches = numeric_limits::max(); +int64_t limit_left = numeric_limits::max(); +bool stdout_is_tty = false; +static bool in_forked_child = false; + +steady_clock::time_point start; +ZSTD_DDict *ddict = nullptr; + +class Corpus { +public: + Corpus(int fd, IOUringEngine *engine); + ~Corpus(); + void find_trigram(uint32_t trgm, function cb); + void get_compressed_filename_block(uint32_t docid, function cb) const; + size_t get_num_filename_blocks() const; + off_t offset_for_block(uint32_t docid) const + { + return hdr.filename_index_offset_bytes + docid * sizeof(uint64_t); + } + const Header &get_hdr() const { return hdr; } + +public: + const int fd; + IOUringEngine *const engine; + + Header hdr; +}; + +Corpus::Corpus(int fd, IOUringEngine *engine) + : fd(fd), engine(engine) { - if (idx < s.size()) { - return (unsigned char)s[idx]; - } else { - return 0; + if (flush_cache) { + off_t len = lseek(fd, 0, SEEK_END); + if (len == -1) { + perror("lseek"); + exit(1); + } + posix_fadvise(fd, 0, len, POSIX_FADV_DONTNEED); + } + + complete_pread(fd, &hdr, sizeof(hdr), /*offset=*/0); + if (memcmp(hdr.magic, "\0plocate", 8) != 0) { + fprintf(stderr, "plocate.db is corrupt or an old version; please rebuild it.\n"); + exit(1); + } + if (hdr.version != 0 && hdr.version != 1) { + fprintf(stderr, "plocate.db has version %u, expected 0 or 1; please rebuild it.\n", hdr.version); + exit(1); + } + if (hdr.version == 0) { + // These will be junk data. + hdr.zstd_dictionary_offset_bytes = 0; + hdr.zstd_dictionary_length_bytes = 0; + } + if (hdr.max_version < 2) { + // This too. (We ignore the other max_version 2 fields.) + hdr.check_visibility = true; } } -static inline uint32_t read_trigram(const string &s, size_t start) +Corpus::~Corpus() { - return read_unigram(s, start) | - (read_unigram(s, start + 1) << 8) | - (read_unigram(s, start + 2) << 16); + close(fd); } -bool has_access(const char *filename, unordered_map *access_rx_cache) +void Corpus::find_trigram(uint32_t trgm, function cb) { - const char *end = strchr(filename + 1, '/'); - while (end != nullptr) { - string parent_path(filename, end); - auto it = access_rx_cache->find(parent_path); - bool ok; - if (it == access_rx_cache->end()) { - ok = access(parent_path.c_str(), R_OK | X_OK) == 0; - access_rx_cache->emplace(move(parent_path), ok); - } else { - ok = it->second; - } - if (!ok) { - return false; + uint32_t bucket = hash_trigram(trgm, hdr.hashtable_size); + engine->submit_read(fd, sizeof(Trigram) * (hdr.extra_ht_slots + 2), hdr.hash_table_offset_bytes + sizeof(Trigram) * bucket, [this, trgm, cb{ move(cb) }](string_view s) { + const Trigram *trgmptr = reinterpret_cast(s.data()); + for (unsigned i = 0; i < hdr.extra_ht_slots + 1; ++i) { + if (trgmptr[i].trgm == trgm) { + cb(trgmptr + i, trgmptr[i + 1].offset - trgmptr[i].offset); + return; + } } - end = strchr(end + 1, '/'); - } -#if 0 - // Check for rx first in the cache; if that isn't true, check R_OK uncached. - // This is roughly the same thing as mlocate does. - auto it = access_rx_cache->find(filename); - if (it != access_rx_cache->end() && it->second) { - return true; - } + // Not found. + cb(nullptr, 0); + }); +} - return access(filename, R_OK) == 0; -#endif - return true; +void Corpus::get_compressed_filename_block(uint32_t docid, function cb) const +{ + // Read the file offset from this docid and the next one. + // This is always allowed, since we have a sentinel block at the end. + engine->submit_read(fd, sizeof(uint64_t) * 2, offset_for_block(docid), [this, cb{ move(cb) }](string_view s) { + const uint64_t *ptr = reinterpret_cast(s.data()); + off_t offset = ptr[0]; + size_t len = ptr[1] - ptr[0]; + engine->submit_read(fd, len, offset, cb); + }); } -struct Trigram { - uint32_t trgm; - uint32_t num_docids; - uint64_t offset; -}; +size_t Corpus::get_num_filename_blocks() const +{ + return hdr.num_docids; +} -size_t scan_docid(const string &needle, uint32_t docid, const char *data, const uint64_t *filename_offsets, unordered_map *access_rx_cache) +void scan_file_block(const vector &needles, string_view compressed, + AccessRXCache *access_rx_cache, uint64_t seq, ResultReceiver *serializer, + atomic *matched) { - const char *compressed = (const char *)(data + filename_offsets[docid]); - size_t compressed_size = filename_offsets[docid + 1] - filename_offsets[docid]; // Allowed we have a sentinel block at the end. - size_t matched = 0; + unsigned long long uncompressed_len = ZSTD_getFrameContentSize(compressed.data(), compressed.size()); + if (uncompressed_len == ZSTD_CONTENTSIZE_UNKNOWN || uncompressed_len == ZSTD_CONTENTSIZE_ERROR) { + fprintf(stderr, "ZSTD_getFrameContentSize() failed\n"); + exit(1); + } string block; - block.resize(ZSTD_getFrameContentSize(compressed, compressed_size) + 1); + block.resize(uncompressed_len + 1); + + static thread_local ZSTD_DCtx *ctx = ZSTD_createDCtx(); // Reused across calls. + size_t err; - ZSTD_decompress(&block[0], block.size(), compressed, compressed_size); + if (ddict != nullptr) { + err = ZSTD_decompress_usingDDict(ctx, &block[0], block.size(), compressed.data(), + compressed.size(), ddict); + } else { + err = ZSTD_decompressDCtx(ctx, &block[0], block.size(), compressed.data(), + compressed.size()); + } + if (ZSTD_isError(err)) { + fprintf(stderr, "ZSTD_decompress(): %s\n", ZSTD_getErrorName(err)); + exit(1); + } block[block.size() - 1] = '\0'; + auto test_candidate = [&](const char *filename, uint64_t local_seq, uint64_t next_seq) { + access_rx_cache->check_access(filename, /*allow_async=*/true, [matched, serializer, local_seq, next_seq, filename{ strdup(filename) }](bool ok) { + if (ok) { + ++*matched; + serializer->print(local_seq, next_seq - local_seq, filename); + } else { + serializer->print(local_seq, next_seq - local_seq, ""); + } + free(filename); + }); + }; + + // We need to know the next sequence number before inserting into Serializer, + // so always buffer one candidate. + const char *pending_candidate = nullptr; + + uint64_t local_seq = seq << 32; for (const char *filename = block.data(); - filename != block.data() + block.size(); - filename += strlen(filename) + 1) { - if (strstr(filename, needle.c_str()) == nullptr) { - continue; + filename != block.data() + block.size(); + filename += strlen(filename) + 1) { + const char *haystack = filename; + if (match_basename) { + haystack = strrchr(filename, '/'); + if (haystack == nullptr) { + haystack = filename; + } else { + ++haystack; + } } - if (has_access(filename, access_rx_cache)) { - ++matched; - printf("%s\n", filename); + + bool found = true; + for (const Needle &needle : needles) { + if (!matches(needle, haystack)) { + found = false; + break; + } + } + if (found) { + if (pending_candidate != nullptr) { + test_candidate(pending_candidate, local_seq, local_seq + 1); + ++local_seq; + } + pending_candidate = filename; } } + if (pending_candidate == nullptr) { + serializer->print(seq << 32, 1ULL << 32, ""); + } else { + test_candidate(pending_candidate, local_seq, (seq + 1) << 32); + } +} + +size_t scan_docids(const vector &needles, const vector &docids, const Corpus &corpus, IOUringEngine *engine) +{ + Serializer docids_in_order; + AccessRXCache access_rx_cache(engine, corpus.get_hdr().check_visibility); + atomic matched{ 0 }; + for (size_t i = 0; i < docids.size(); ++i) { + uint32_t docid = docids[i]; + corpus.get_compressed_filename_block(docid, [i, &matched, &needles, &access_rx_cache, &docids_in_order](string_view compressed) { + scan_file_block(needles, compressed, &access_rx_cache, i, &docids_in_order, &matched); + }); + } + engine->finish(); return matched; } -void do_search_file(const string &needle, const char *filename) +struct WorkerThread { + thread t; + + // We use a result queue instead of synchronizing Serializer, + // since a lock on it becomes a huge choke point if there are + // lots of threads. + mutex result_mu; + struct Result { + uint64_t seq; + uint64_t skip; + string msg; + }; + vector results; +}; + +class WorkerThreadReceiver : public ResultReceiver { +public: + WorkerThreadReceiver(WorkerThread *wt) + : wt(wt) {} + + void print(uint64_t seq, uint64_t skip, const string msg) override + { + lock_guard lock(wt->result_mu); + if (msg.empty() && !wt->results.empty() && wt->results.back().seq + wt->results.back().skip == seq) { + wt->results.back().skip += skip; + } else { + wt->results.emplace_back(WorkerThread::Result{ seq, skip, move(msg) }); + } + } + +private: + WorkerThread *wt; +}; + +void deliver_results(WorkerThread *wt, Serializer *serializer) +{ + vector results; + { + lock_guard lock(wt->result_mu); + results = move(wt->results); + } + for (const WorkerThread::Result &result : results) { + serializer->print(result.seq, result.skip, move(result.msg)); + } +} + +// We do this sequentially, as it's faster than scattering +// a lot of I/O through io_uring and hoping the kernel will +// coalesce it plus readahead for us. Since we assume that +// we will primarily be CPU-bound, we'll be firing up one +// worker thread for each spare core (the last one will +// only be doing I/O). access() is still synchronous. +uint64_t scan_all_docids(const vector &needles, int fd, const Corpus &corpus) { - int fd = open(filename, O_RDONLY); + { + const Header &hdr = corpus.get_hdr(); + if (hdr.zstd_dictionary_length_bytes > 0) { + string dictionary; + dictionary.resize(hdr.zstd_dictionary_length_bytes); + complete_pread(fd, &dictionary[0], hdr.zstd_dictionary_length_bytes, hdr.zstd_dictionary_offset_bytes); + ddict = ZSTD_createDDict(dictionary.data(), dictionary.size()); + } + } + + AccessRXCache access_rx_cache(nullptr, corpus.get_hdr().check_visibility); + Serializer serializer; + uint32_t num_blocks = corpus.get_num_filename_blocks(); + unique_ptr offsets(new uint64_t[num_blocks + 1]); + complete_pread(fd, offsets.get(), (num_blocks + 1) * sizeof(uint64_t), corpus.offset_for_block(0)); + atomic matched{ 0 }; + + mutex mu; + condition_variable queue_added, queue_removed; + deque> work_queue; // Under mu. + bool done = false; // Under mu. + + unsigned num_threads = max(sysconf(_SC_NPROCESSORS_ONLN) - 1, 1); + dprintf("Using %u worker threads for linear scan.\n", num_threads); + unique_ptr threads(new WorkerThread[num_threads]); + for (unsigned i = 0; i < num_threads; ++i) { + threads[i].t = thread([&threads, &mu, &queue_added, &queue_removed, &work_queue, &done, &offsets, &needles, &access_rx_cache, &matched, i] { + // regcomp() takes a lock on the regex, so each thread will need its own. + const vector *use_needles = &needles; + vector recompiled_needles; + if (i != 0 && patterns_are_regex) { + recompiled_needles = needles; + for (Needle &needle : recompiled_needles) { + needle.re = compile_regex(needle.str); + } + use_needles = &recompiled_needles; + } + + WorkerThreadReceiver receiver(&threads[i]); + for (;;) { + uint32_t io_docid, last_docid; + string compressed; + + { + unique_lock lock(mu); + queue_added.wait(lock, [&work_queue, &done] { return !work_queue.empty() || done; }); + if (done && work_queue.empty()) { + return; + } + tie(io_docid, last_docid, compressed) = move(work_queue.front()); + work_queue.pop_front(); + queue_removed.notify_all(); + } + + for (uint32_t docid = io_docid; docid < last_docid; ++docid) { + size_t relative_offset = offsets[docid] - offsets[io_docid]; + size_t len = offsets[docid + 1] - offsets[docid]; + scan_file_block(*use_needles, { &compressed[relative_offset], len }, &access_rx_cache, docid, &receiver, &matched); + } + } + }); + } + + string compressed; + for (uint32_t io_docid = 0; io_docid < num_blocks; io_docid += 32) { + uint32_t last_docid = std::min(io_docid + 32, num_blocks); + size_t io_len = offsets[last_docid] - offsets[io_docid]; + if (compressed.size() < io_len) { + compressed.resize(io_len); + } + complete_pread(fd, &compressed[0], io_len, offsets[io_docid]); + + { + unique_lock lock(mu); + queue_removed.wait(lock, [&work_queue] { return work_queue.size() < 256; }); // Allow ~2MB of data queued up. + work_queue.emplace_back(io_docid, last_docid, move(compressed)); + queue_added.notify_one(); // Avoid the thundering herd. + } + + // Pick up some results, so that we are sure that we won't just overload. + // (Seemingly, going through all of these causes slowness with many threads, + // but taking only one is OK.) + unsigned i = io_docid / 32; + deliver_results(&threads[i % num_threads], &serializer); + } + { + lock_guard lock(mu); + done = true; + queue_added.notify_all(); + } + for (unsigned i = 0; i < num_threads; ++i) { + threads[i].t.join(); + deliver_results(&threads[i], &serializer); + } + return matched; +} + +// Takes the given posting list, unions it into the parts of the trigram disjunction +// already read; if the list is complete, intersects with “cur_candidates”. +// +// Returns true if the search should be aborted (we are done). +bool new_posting_list_read(TrigramDisjunction *td, vector decoded, vector *cur_candidates, vector *tmp) +{ + if (td->docids.empty()) { + td->docids = move(decoded); + } else { + tmp->clear(); + set_union(decoded.begin(), decoded.end(), td->docids.begin(), td->docids.end(), back_inserter(*tmp)); + swap(*tmp, td->docids); + } + if (--td->remaining_trigrams_to_read > 0) { + // Need to wait for more. + if (ignore_case) { + dprintf(" ... %u reads left in OR group %u (%zu docids in list)\n", + td->remaining_trigrams_to_read, td->index, td->docids.size()); + } + return false; + } + if (cur_candidates->empty()) { + if (ignore_case) { + dprintf(" ... all reads done for OR group %u (%zu docids)\n", + td->index, td->docids.size()); + } + *cur_candidates = move(td->docids); + } else { + tmp->clear(); + set_intersection(cur_candidates->begin(), cur_candidates->end(), + td->docids.begin(), td->docids.end(), + back_inserter(*tmp)); + swap(*cur_candidates, *tmp); + if (ignore_case) { + if (cur_candidates->empty()) { + dprintf(" ... all reads done for OR group %u (%zu docids), intersected (none left, search is done)\n", + td->index, td->docids.size()); + return true; + } else { + dprintf(" ... all reads done for OR group %u (%zu docids), intersected (%zu left)\n", + td->index, td->docids.size(), cur_candidates->size()); + } + } + } + return false; +} + +uint64_t do_search_file(const vector &needles, const std::string &filename) +{ + int fd = open(filename.c_str(), O_RDONLY); if (fd == -1) { - perror(filename); + perror(filename.c_str()); exit(1); } @@ -116,105 +460,508 @@ void do_search_file(const string &needle, const char *filename) exit(EXIT_FAILURE); } - //steady_clock::time_point start = steady_clock::now(); + start = steady_clock::now(); if (access("/", R_OK | X_OK)) { // We can't find anything, no need to bother... - return; + return 0; } - off_t len = lseek(fd, 0, SEEK_END); - if (len == -1) { - perror("lseek"); - exit(1); - } - const char *data = (char *)mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, /*offset=*/0); - if (data == MAP_FAILED) { - perror("mmap"); - exit(1); + IOUringEngine engine(/*slop_bytes=*/16); // 16 slop bytes as described in turbopfor.h. + Corpus corpus(fd, &engine); + dprintf("Corpus init done after %.1f ms.\n", 1e3 * duration(steady_clock::now() - start).count()); + + vector trigram_groups; + if (patterns_are_regex) { + // We could parse the regex to find trigrams that have to be there + // (there are actually known algorithms to deal with disjunctions + // and such, too), but for now, we just go brute force. + // Using locate with regexes is pretty niche. + } else { + for (const Needle &needle : needles) { + parse_trigrams(needle.str, ignore_case, &trigram_groups); + } } - uint64_t num_trigrams = *(const uint64_t *)data; - uint64_t filename_index_offset = *(const uint64_t *)(data + sizeof(uint64_t)); - const Trigram *trgm_begin = (Trigram *)(data + sizeof(uint64_t) * 2); - const Trigram *trgm_end = trgm_begin + num_trigrams; + unique_sort( + &trigram_groups, + [](const TrigramDisjunction &a, const TrigramDisjunction &b) { return a.trigram_alternatives < b.trigram_alternatives; }, + [](const TrigramDisjunction &a, const TrigramDisjunction &b) { return a.trigram_alternatives == b.trigram_alternatives; }); - vector trigrams; - for (size_t i = 0; i < needle.size() - 2; ++i) { - uint32_t trgm = read_trigram(needle, i); - const Trigram *trgmptr = lower_bound(trgm_begin, trgm_end, trgm, [](const Trigram &trgm, uint32_t t) { - return trgm.trgm < t; - }); - if (trgmptr == trgm_end || trgmptr->trgm != trgm) { - dprintf("trigram %06x isn't found, we abort the search\n", trgm); - munmap((void *)data, len); - close(fd); - return; + // Give them names for debugging. + unsigned td_index = 0; + for (TrigramDisjunction &td : trigram_groups) { + td.index = td_index++; + } + + // Collect which trigrams we need to look up in the hash table. + unordered_map> trigrams_to_lookup; + for (TrigramDisjunction &td : trigram_groups) { + for (uint32_t trgm : td.trigram_alternatives) { + trigrams_to_lookup[trgm].push_back(&td); } - trigrams.push_back(trgmptr); } - sort(trigrams.begin(), trigrams.end()); + if (trigrams_to_lookup.empty()) { + // Too short for trigram matching. Apply brute force. + // (We could have searched through all trigrams that matched + // the pattern and done a union of them, but that's a lot of + // work for fairly unclear gain.) + uint64_t matched = scan_all_docids(needles, fd, corpus); + dprintf("Done in %.1f ms, found %" PRId64 " matches.\n", + 1e3 * duration(steady_clock::now() - start).count(), matched); + return matched; + } + + // Sneak in fetching the dictionary, if present. It's not necessarily clear + // exactly where it would be cheapest to get it, but it needs to be present + // before we can decode any of the posting lists. Most likely, it's + // in the same filesystem block as the header anyway, so it should be + // present in the cache. { - auto last = unique(trigrams.begin(), trigrams.end()); - trigrams.erase(last, trigrams.end()); + const Header &hdr = corpus.get_hdr(); + if (hdr.zstd_dictionary_length_bytes > 0) { + engine.submit_read(fd, hdr.zstd_dictionary_length_bytes, hdr.zstd_dictionary_offset_bytes, [](string_view s) { + ddict = ZSTD_createDDict(s.data(), s.size()); + dprintf("Dictionary initialized after %.1f ms.\n", 1e3 * duration(steady_clock::now() - start).count()); + }); + } } - sort(trigrams.begin(), trigrams.end(), [&](const Trigram *a, const Trigram *b) { - return a->num_docids < b->num_docids; - }); - vector in1, in2, out; - for (const Trigram *trgmptr : trigrams) { - //uint32_t trgm = trgmptr->trgm; - size_t num = trgmptr->num_docids; - unsigned char *pldata = (unsigned char *)(data + trgmptr->offset); - if (in1.empty()) { - in1.resize(num + 128); - p4nd1dec128v32((unsigned char *)pldata, num, &in1[0]); - in1.resize(num); - dprintf("trigram '%c%c%c' decoded to %zu entries\n", trgm & 0xff, (trgm >> 8) & 0xff, (trgm >> 16) & 0xff, num); - } else { - if (num > in1.size() * 100) { - dprintf("trigram '%c%c%c' has %zu entries, ignoring the rest (will weed out false positives later)\n", - trgm & 0xff, (trgm >> 8) & 0xff, (trgm >> 16) & 0xff, num); - break; + // Look them all up on disk. + bool should_early_exit = false; + for (auto &[trgm, trigram_groups] : trigrams_to_lookup) { + corpus.find_trigram(trgm, [trgm{ trgm }, trigram_groups{ &trigram_groups }, &should_early_exit](const Trigram *trgmptr, size_t len) { + if (trgmptr == nullptr) { + dprintf("trigram %s isn't found\n", print_trigram(trgm).c_str()); + for (TrigramDisjunction *td : *trigram_groups) { + --td->remaining_trigrams_to_read; + + // If we now know this trigram group doesn't match anything at all, + // we can do early exit; however, if we're in a forked child, + // that would confuse the parent process (since we don't write + // our count to the pipe), so we wait until we're back in to the + // regular (non-async) context. This is a fairly rare case anyway, + // and the gains from dropping the remaining trigram reads are limited. + if (td->remaining_trigrams_to_read == 0 && td->read_trigrams.empty()) { + if (in_forked_child) { + should_early_exit = true; + } else { + dprintf("zero matches in %s, so we are done\n", print_td(*td).c_str()); + if (only_count) { + printf("0\n"); + } + exit(0); + } + } + } + return; + } + for (TrigramDisjunction *td : *trigram_groups) { + --td->remaining_trigrams_to_read; + td->max_num_docids += trgmptr->num_docids; + td->read_trigrams.emplace_back(*trgmptr, len); } + }); + } + engine.finish(); + dprintf("Hashtable lookups done after %.1f ms.\n", 1e3 * duration(steady_clock::now() - start).count()); + + if (should_early_exit) { + return 0; + } + + for (TrigramDisjunction &td : trigram_groups) { + // Reset for reads. + td.remaining_trigrams_to_read = td.read_trigrams.size(); + + if (ignore_case) { // If case-sensitive, they'll all be pretty obvious single-entry groups. + dprintf("OR group %u (max_num_docids=%u): %s\n", td.index, td.max_num_docids, print_td(td).c_str()); + } + } + + // TODO: For case-insensitive (ie. more than one alternative in each), + // prioritize the ones with fewer seeks? + sort(trigram_groups.begin(), trigram_groups.end(), + [&](const TrigramDisjunction &a, const TrigramDisjunction &b) { + return a.max_num_docids < b.max_num_docids; + }); + + unordered_map> uses_trigram; + for (TrigramDisjunction &td : trigram_groups) { + for (uint32_t trgm : td.trigram_alternatives) { + uses_trigram[trgm].push_back(&td); + } + } + + unordered_set trigrams_submitted_read; + vector cur_candidates, tmp, decoded; + bool done = false; + for (TrigramDisjunction &td : trigram_groups) { + if (!cur_candidates.empty() && td.max_num_docids > cur_candidates.size() * 100) { + dprintf("%s has up to %u entries, ignoring the rest (will " + "weed out false positives later)\n", + print_td(td).c_str(), td.max_num_docids); + break; + } - if (in2.size() < num + 128) { - in2.resize(num + 128); + for (auto &[trgmptr, len] : td.read_trigrams) { + if (trigrams_submitted_read.count(trgmptr.trgm) != 0) { + continue; } - p4nd1dec128v32((unsigned char *)pldata, num, &in2[0]); - - out.clear(); - set_intersection(in1.begin(), in1.end(), in2.begin(), in2.begin() + num, back_inserter(out)); - swap(in1, out); - dprintf("trigram '%c%c%c' decoded to %zu entries, %zu left\n", trgm & 0xff, (trgm >> 8) & 0xff, (trgm >> 16) & 0xff, num, in1.size()); - if (in1.empty()) { - dprintf("no matches (intersection list is empty)\n"); - break; + trigrams_submitted_read.insert(trgmptr.trgm); + // Only stay a certain amount ahead, so that we don't spend I/O + // on reading the latter, large posting lists. We are unlikely + // to need them anyway, even if they should come in first. + if (engine.get_waiting_reads() >= 5) { + engine.finish(); + if (done) + break; } + engine.submit_read(fd, len, trgmptr.offset, [trgmptr{ trgmptr }, len{ len }, &done, &cur_candidates, &tmp, &decoded, &uses_trigram](string_view s) { + if (done) + return; + + uint32_t trgm = trgmptr.trgm; + const unsigned char *pldata = reinterpret_cast(s.data()); + size_t num = trgmptr.num_docids; + decoded.resize(num); + decode_pfor_delta1_128(pldata, num, /*interleaved=*/true, &decoded[0]); + + assert(uses_trigram.count(trgm) != 0); + bool was_empty = cur_candidates.empty(); + if (ignore_case) { + dprintf("trigram %s (%zu bytes) decoded to %zu entries\n", print_trigram(trgm).c_str(), len, num); + } + + for (TrigramDisjunction *td : uses_trigram[trgm]) { + done |= new_posting_list_read(td, decoded, &cur_candidates, &tmp); + if (done) + break; + } + if (!ignore_case) { + if (was_empty) { + dprintf("trigram %s (%zu bytes) decoded to %zu entries\n", print_trigram(trgm).c_str(), len, num); + } else if (cur_candidates.empty()) { + dprintf("trigram %s (%zu bytes) decoded to %zu entries (none left, search is done)\n", print_trigram(trgm).c_str(), len, num); + } else { + dprintf("trigram %s (%zu bytes) decoded to %zu entries (%zu left)\n", print_trigram(trgm).c_str(), len, num, cur_candidates.size()); + } + } + }); } } - steady_clock::time_point end = steady_clock::now(); + engine.finish(); + if (done) { + return 0; + } + dprintf("Intersection done after %.1f ms. Doing final verification and printing:\n", + 1e3 * duration(steady_clock::now() - start).count()); - dprintf("Intersection took %.1f ms. Doing final verification and printing:\n", - 1e3 * duration(end - start).count()); + uint64_t matched = scan_docids(needles, cur_candidates, corpus, &engine); + dprintf("Done in %.1f ms, found %" PRId64 " matches.\n", + 1e3 * duration(steady_clock::now() - start).count(), matched); + return matched; +} - unordered_map access_rx_cache; +// Run do_search_file() in a child process. +// +// The reason for this is that we're not robust against malicious input, so we need +// to drop privileges after opening the file. (Otherwise, we could fall prey to an attack +// where a user does locate -d badfile.db:/var/lib/plocate/plocate.db, badfile.db contains +// a buffer overflow that takes over the process, and then uses the elevated privileges +// to print out otherwise inaccessible paths.) We solve this by forking and treating the +// child process as untrusted after it has dropped its privileges (which it does before +// reading any data from the file); it returns a single 64-bit number over a pipe, +// and that's it. The parent keeps its privileges, and can then fork out new children +// without fear of being taken over. (The child keeps stdout for outputting results.) +// +// The count is returned over the pipe, because it's needed both for --limit and --count. +uint64_t do_search_file_in_child(const vector &needles, const std::string &filename) +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + perror("pipe"); + exit(EXIT_FAILURE); + } - const uint64_t *filename_offsets = (const uint64_t *)(data + filename_index_offset); - int matched = 0; - for (uint32_t docid : in1) { - matched += scan_docid(needle, docid, data, filename_offsets, &access_rx_cache); + pid_t child_pid = fork(); + switch (child_pid) { + case 0: { + // Child. + close(pipefd[0]); + in_forked_child = true; + uint64_t matched = do_search_file(needles, filename); + int ret; + do { + ret = write(pipefd[1], &matched, sizeof(matched)); + } while (ret == -1 && errno == EINTR); + if (ret != sizeof(matched)) { + perror("write"); + _exit(EXIT_FAILURE); + } + _exit(EXIT_SUCCESS); + } + case -1: + // Error. + perror("fork"); + exit(EXIT_FAILURE); + default: + // Parent. + close(pipefd[1]); + break; } - end = steady_clock::now(); - dprintf("Done in %.1f ms, found %d matches.\n", - 1e3 * duration(end - start).count(), matched); - munmap((void *)data, len); - close(fd); + // Wait for the child to finish. + int wstatus; + pid_t err; + do { + err = waitpid(child_pid, &wstatus, 0); + } while (err == -1 && errno == EINTR); + if (err == -1) { + perror("waitpid"); + exit(EXIT_FAILURE); + } + if (WIFEXITED(wstatus)) { + if (WEXITSTATUS(wstatus) != 0) { + // The child has probably already printed out its error, so just propagate the exit status. + exit(WEXITSTATUS(wstatus)); + } + // Success! + } else if (!WIFEXITED(wstatus)) { + fprintf(stderr, "FATAL: Child died unexpectedly while processing %s\n", filename.c_str()); + exit(1); + } + + // Now get the number of matches from the child. + uint64_t matched; + int ret; + do { + ret = read(pipefd[0], &matched, sizeof(matched)); + } while (ret == -1 && errno == EINTR); + if (ret == -1) { + perror("read"); + exit(EXIT_FAILURE); + } else if (ret != sizeof(matched)) { + fprintf(stderr, "FATAL: Short read through pipe (got %d bytes)\n", ret); + exit(EXIT_FAILURE); + } + close(pipefd[0]); + return matched; +} + +// Parses a colon-separated list of strings and appends them onto the given vector. +// Backslash escapes whatever comes after it. +void parse_dbpaths(const char *ptr, vector *output) +{ + string str; + while (*ptr != '\0') { + if (*ptr == '\\') { + if (ptr[1] == '\0') { + fprintf(stderr, "ERROR: Escape character at the end of string\n"); + exit(EXIT_FAILURE); + } + // Escape. + str.push_back(ptr[1]); + ptr += 2; + continue; + } + if (*ptr == ':') { + // Separator. + output->push_back(move(str)); + ++ptr; + continue; + } + str.push_back(*ptr++); + } + output->push_back(move(str)); +} + +void usage() +{ + printf( + "Usage: plocate [OPTION]... PATTERN...\n" + "\n" + " -b, --basename search only the file name portion of path names\n" + " -c, --count print number of matches instead of the matches\n" + " -d, --database DBPATH search for files in DBPATH\n" + " (default is " DBFILE ")\n" + " -i, --ignore-case search case-insensitively\n" + " -l, --limit LIMIT stop after LIMIT matches\n" + " -0, --null delimit matches by NUL instead of newline\n" + " -r, --regexp interpret patterns as basic regexps (slow)\n" + " --regex interpret patterns as extended regexps (slow)\n" + " -w, --wholename search the entire path name (default; see -b)\n" + " --help print this help\n" + " --version print version information\n"); +} + +void version() +{ + printf("%s %s\n", PACKAGE_NAME, PACKAGE_VERSION); + printf("Copyright 2020 Steinar H. Gunderson\n"); + printf("License GPLv2+: GNU GPL version 2 or later .\n"); + printf("This is free software: you are free to change and redistribute it.\n"); + printf("There is NO WARRANTY, to the extent permitted by law.\n"); + exit(0); } int main(int argc, char **argv) { - //do_search_file(argv[1], "all.trgm"); - do_search_file(argv[1], "/var/lib/mlocate/plocate.db"); + vector dbpaths; + + constexpr int EXTENDED_REGEX = 1000; + constexpr int FLUSH_CACHE = 1001; + static const struct option long_options[] = { + { "help", no_argument, 0, 'h' }, + { "count", no_argument, 0, 'c' }, + { "basename", no_argument, 0, 'b' }, + { "database", required_argument, 0, 'd' }, + { "ignore-case", no_argument, 0, 'i' }, + { "limit", required_argument, 0, 'l' }, + { "null", no_argument, 0, '0' }, + { "version", no_argument, 0, 'V' }, + { "regexp", no_argument, 0, 'r' }, + { "regex", no_argument, 0, EXTENDED_REGEX }, + { "wholename", no_argument, 0, 'w' }, + { "debug", no_argument, 0, 'D' }, // Not documented. + // Enable to test cold-cache behavior (except for access()). Not documented. + { "flush-cache", no_argument, 0, FLUSH_CACHE }, + { 0, 0, 0, 0 } + }; + + setlocale(LC_ALL, ""); + for (;;) { + int option_index = 0; + int c = getopt_long(argc, argv, "bcd:hil:n:0rwVD", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'b': + match_basename = true; + break; + case 'c': + only_count = true; + break; + case 'd': + parse_dbpaths(optarg, &dbpaths); + break; + case 'h': + usage(); + exit(0); + case 'i': + ignore_case = true; + break; + case 'l': + case 'n': + limit_matches = limit_left = atoll(optarg); + if (limit_matches <= 0) { + fprintf(stderr, "Error: limit must be a strictly positive number.\n"); + exit(1); + } + break; + case '0': + print_nul = true; + break; + case 'r': + patterns_are_regex = true; + break; + case EXTENDED_REGEX: + patterns_are_regex = true; + use_extended_regex = true; + break; + case 'w': + match_basename = false; // No-op unless -b is given first. + break; + case 'D': + use_debug = true; + break; + case FLUSH_CACHE: + flush_cache = true; + break; + case 'V': + version(); + break; + default: + exit(1); + } + } + + if (use_debug || flush_cache) { + // Debug information would leak information about which files exist, + // so drop setgid before we open the file; one would either need to run + // as root, or use a locally-built file. Doing the same thing for + // flush_cache is mostly paranoia, in an attempt to prevent random users + // from making plocate slow for everyone else. + if (setgid(getgid()) != 0) { + perror("setgid"); + exit(EXIT_FAILURE); + } + } + + if (!print_nul) { + stdout_is_tty = isatty(1); + } + + vector needles; + for (int i = optind; i < argc; ++i) { + Needle needle; + needle.str = argv[i]; + + // See if there are any wildcard characters, which indicates we should treat it + // as an (anchored) glob. + bool any_wildcard = false; + for (size_t i = 0; i < needle.str.size(); i += read_unigram(needle.str, i).second) { + if (read_unigram(needle.str, i).first == WILDCARD_UNIGRAM) { + any_wildcard = true; + break; + } + } + + if (patterns_are_regex) { + needle.type = Needle::REGEX; + needle.re = compile_regex(needle.str); + } else if (any_wildcard) { + needle.type = Needle::GLOB; + } else if (ignore_case) { + // strcasestr() doesn't handle locales correctly (even though LSB + // claims it should), but somehow, fnmatch() does, and it's about + // the same speed as using a regex. + needle.type = Needle::GLOB; + needle.str = "*" + needle.str + "*"; + } else { + needle.type = Needle::STRSTR; + needle.str = unescape_glob_to_plain_string(needle.str); + } + needles.push_back(move(needle)); + } + if (needles.empty()) { + fprintf(stderr, "plocate: no pattern to search for specified\n"); + exit(0); + } + + if (dbpaths.empty()) { + // No -d given, so use our default. Note that this happens + // even if LOCATE_PATH exists, to match mlocate behavior. + dbpaths.push_back(DBFILE); + } + + const char *locate_path = getenv("LOCATE_PATH"); + if (locate_path != nullptr) { + parse_dbpaths(locate_path, &dbpaths); + } + + uint64_t matched = 0; + for (size_t i = 0; i < dbpaths.size(); ++i) { + uint64_t this_matched; + if (i != dbpaths.size() - 1) { + this_matched = do_search_file_in_child(needles, dbpaths[i]); + } else { + this_matched = do_search_file(needles, dbpaths[i]); + } + matched += this_matched; + limit_left -= this_matched; + } + if (only_count) { + printf("%" PRId64 "\n", matched); + } }