Skip to content
API Documentation
BastionGuard™
fast_filter.h
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#pragma once
22#include <cstdint>
23#include <string>
24#include <vector>
25#include <unordered_map>
26#include <unordered_set>
27#include <shared_mutex>
28#include <fstream>
29#include <sstream>
30#include <deque>
31#include <memory>
32#include <optional>
33#include <chrono>
34#include <mutex>
35#include <algorithm>
36#include <list>
37#include <cmath>
38#include <cctype>
39#include <sqlite3.h>
40
41#ifdef FASTFILTER_USE_RE2
42 #include <re2/re2.h>
43#endif
44
45namespace fastfilter {
46
47
48static inline uint64_t rotl64(uint64_t x, int8_t r) { return (x << r) | (x >> (64 - r)); }
49static inline uint64_t fmix64(uint64_t k){
50 k ^= k >> 33; k *= 0xff51afd7ed558ccdULL; k ^= k >> 33; k *= 0xc4ceb9fe1a85ec53ULL; k ^= k >> 33; return k;
51}
52static inline uint64_t murmur3_64(const void* key, size_t len, uint32_t seed=0xB00B1E){
53 const uint8_t* data = (const uint8_t*)key; const int nblocks = len / 16;
54 uint64_t h1 = seed, h2 = seed;
55 const uint64_t c1 = 0x87c37b91114253d5ULL, c2 = 0x4cf5ad432745937fULL;
56 const uint64_t* blocks = (const uint64_t*)(data);
57 for(int i = 0; i < nblocks; i++){
58 uint64_t k1 = blocks[i*2+0]; uint64_t k2 = blocks[i*2+1];
59 k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; h1 = rotl64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;
60 k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; h2 = rotl64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
61 }
62 const uint8_t* tail = (const uint8_t*)(data + nblocks*16);
63 uint64_t k1 = 0, k2 = 0;
64 switch(len & 15){
65 case 15: k2 ^= ((uint64_t)tail[14]) << 48; [[fallthrough]];
66 case 14: k2 ^= ((uint64_t)tail[13]) << 40; [[fallthrough]];
67 case 13: k2 ^= ((uint64_t)tail[12]) << 32; [[fallthrough]];
68 case 12: k2 ^= ((uint64_t)tail[11]) << 24; [[fallthrough]];
69 case 11: k2 ^= ((uint64_t)tail[10]) << 16; [[fallthrough]];
70 case 10: k2 ^= ((uint64_t)tail[9]) << 8; [[fallthrough]];
71 case 9: k2 ^= ((uint64_t)tail[8]) << 0; k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; [[fallthrough]];
72 case 8: k1 ^= ((uint64_t)tail[7]) << 56; [[fallthrough]];
73 case 7: k1 ^= ((uint64_t)tail[6]) << 48; [[fallthrough]];
74 case 6: k1 ^= ((uint64_t)tail[5]) << 40; [[fallthrough]];
75 case 5: k1 ^= ((uint64_t)tail[4]) << 32; [[fallthrough]];
76 case 4: k1 ^= ((uint64_t)tail[3]) << 24; [[fallthrough]];
77 case 3: k1 ^= ((uint64_t)tail[2]) << 16; [[fallthrough]];
78 case 2: k1 ^= ((uint64_t)tail[1]) << 8; [[fallthrough]];
79 case 1: k1 ^= ((uint64_t)tail[0]) << 0; k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; [[fallthrough]];
80 default: break;
81 }
82 h1 ^= len; h2 ^= len; h1 += h2; h2 += h1; h1 = fmix64(h1); h2 = fmix64(h2); h1 += h2;
83 return h1;
84}
85
86
87struct Bloom {
88 size_t m_bits = 0; size_t k_hashes = 0; std::vector<uint8_t> buf;
89 void init(size_t n_items, double fp_rate){
90
91 double ln2 = 0.6931471805599453;
92 double m = -(double)n_items * std::log(fp_rate) / (ln2*ln2);
93 m_bits = (size_t)std::max( (double)8.0, std::ceil(m) );
94 k_hashes = std::max<size_t>(1, (size_t)std::round((m_bits/(double)std::max<size_t>(1,n_items))*ln2));
95 buf.assign((m_bits + 7)/8, 0);
96 }
97 inline void _setbit(uint64_t h){ size_t i = h % m_bits; buf[i>>3] |= (uint8_t)(1u << (i & 7)); }
98 inline bool _getbit(uint64_t h) const { size_t i = h % m_bits; return (buf[i>>3] >> (i & 7)) & 1u; }
99 void add(const std::string& s){
100 uint64_t h1 = murmur3_64(s.data(), s.size(), 0xA5A5A5A5);
101 uint64_t h2 = murmur3_64(s.data(), s.size(), 0x5A5A5A5A);
102 for(size_t i=0;i<k_hashes;i++) _setbit(h1 + i*h2);
103 }
104 bool might_contain(const std::string& s) const {
105 uint64_t h1 = murmur3_64(s.data(), s.size(), 0xA5A5A5A5);
106 uint64_t h2 = murmur3_64(s.data(), s.size(), 0x5A5A5A5A);
107 for(size_t i=0;i<k_hashes;i++) if(!_getbit(h1 + i*h2)) return false; return true;
108 }
109};
110
111
112struct ACNode {
113 std::unordered_map<char,int> next; int fail = 0; bool out = false;
114};
115
116struct Automaton {
117 std::vector<ACNode> g; size_t patterns = 0;
118 Automaton(){ g.reserve(1024); g.push_back(ACNode()); }
119 static inline char norm(char c){ return (char)std::tolower((unsigned char)c); }
120 void add(const std::string& pat){
121 int v = 0; for(char cc : pat){ char c = norm(cc); if(!g[v].next.count(c)){ g[v].next[c] = (int)g.size(); g.push_back(ACNode()); } v = g[v].next[c]; }
122 g[v].out = true; patterns++; }
123 void build(){
124 std::deque<int> q; // BFS
125 for(auto &kv : g[0].next){ q.push_back(kv.second); g[kv.second].fail = 0; }
126 while(!q.empty()){
127 int v = q.front(); q.pop_front();
128 for(auto &kv : g[v].next){ char c = kv.first; int u = kv.second; q.push_back(u);
129 int f = g[v].fail; while(f && !g[f].next.count(c)) f = g[f].fail; if(g[f].next.count(c) && g[f].next[c]!=u) g[u].fail = g[f].next[c]; else g[u].fail = 0; g[u].out = g[u].out || g[g[u].fail].out; }
130 }
131 }
132 bool search_any(const std::string& s) const {
133 int v = 0; for(char cc: s){ char c = norm(cc); while(v && !g[v].next.count(c)) v = g[v].fail; if(g[v].next.count(c)) v = g[v].next.at(c); if(g[v].out) return true; }
134 return false; }
135};
136
137
138struct LRUCache {
139 size_t cap; std::list<uint64_t> order; std::unordered_map<uint64_t, std::pair<bool,std::list<uint64_t>::iterator>> map;
140 explicit LRUCache(size_t c=100000): cap(c){}
141 bool get(uint64_t k, bool &out){ auto it = map.find(k); if(it==map.end()) return false; order.erase(it->second.second); order.push_front(k); it->second.second = order.begin(); out = it->second.first; return true; }
142 void put(uint64_t k, bool v){ auto it = map.find(k); if(it!=map.end()){ it->second.first=v; order.erase(it->second.second); order.push_front(k); it->second.second=order.begin(); return;} if(map.size()>=cap){ uint64_t ev = order.back(); order.pop_back(); map.erase(ev);} order.push_front(k); map.emplace(k, std::make_pair(v, order.begin())); }
143};
144
145
146struct Config { size_t cache_capacity=100000; double bloom_fp_rate=0.01; };
147
148struct Verdict { bool block=false; enum Stage{ NONE, BLOOM, AC, REGEX } stage = NONE; std::string why; };
149
150class Engine {
151public:
152 Engine() : cfg(Config{}), cache(cfg.cache_capacity) {}
153 explicit Engine(const Config& c): cfg(c), cache(c.cache_capacity) {}
154
155
156 bool load_patterns_file(const std::string& path){
157 std::ifstream in(path); if(!in) return false; std::vector<std::string> pats; pats.reserve(100000);
158 std::string line; while(std::getline(in,line)){ trim(line); if(line.empty()||line[0]=='#') continue; tolower_inplace(line); pats.push_back(line); }
159 build_bloom(pats.size()); for(const auto& p: pats){ bloom.add(p);} for(const auto& p: pats){ ac.add(p);} ac.build(); total_patterns = pats.size(); return true;
160 }
161
162 bool load_regex_file(const std::string& path){
163#ifdef FASTFILTER_USE_RE2
164 std::ifstream in(path); if(!in) return false; std::string line; size_t cnt=0; regexes.clear();
165 while(std::getline(in,line)){
166 trim(line); if(line.rfind("Pattern:",0)==0){ std::string pat = line.substr(8); trim(pat); if(!pat.empty()){ regexes.emplace_back(std::make_unique<RE2>(pat)); cnt++; } }
167 }
168 return cnt>0;
169#else
170 (void)path; return false;
171#endif
172 }
173
174 bool load_patterns_sqlite(const std::string& db_path, const std::string& table = "phishing_rules");
175
176 Verdict check_url(const std::string& url){
177 auto norm = lowercase(url);
178 uint64_t key = murmur3_64(norm.data(), norm.size()); bool cv; if(cache.get(key, cv)) return {cv, cv?Verdict::AC:Verdict::NONE, cv?"cache-hit": "cache-hit"};
179 if(!bloom.m_bits || !bloom.might_contain(norm)){ cache.put(key,false); return {false, Verdict::BLOOM, "bloom-negative"}; }
180 bool hit = ac.search_any(norm);
181 if(!hit){ cache.put(key,false); return {false, Verdict::AC, "ac-nohit"}; }
182#ifdef FASTFILTER_USE_RE2
183 // stage 3: regex (if any)
184 for(const auto& r: regexes){ if(RE2::PartialMatch(norm, *r)){ cache.put(key,true); return {true, Verdict::REGEX, "regex-match"}; } }
185 cache.put(key,true); return {true, Verdict::AC, "ac-match"};
186#else
187 cache.put(key,true); return {true, Verdict::AC, "ac-match"};
188#endif
189 }
190
191 size_t get_pattern_count() const { return total_patterns; }
192
193private:
195#ifdef FASTFILTER_USE_RE2
196 std::vector<std::unique_ptr<RE2>> regexes;
197#endif
198
199 void build_bloom(size_t n){ bloom.init(n>0?n:1, cfg.bloom_fp_rate); }
200
201 static inline void trim(std::string& s){ size_t a=0; while(a<s.size() && std::isspace((unsigned char)s[a])) a++; size_t b=s.size(); while(b>a && std::isspace((unsigned char)s[b-1])) b--; s.assign(s.substr(a,b-a)); }
202 static inline void tolower_inplace(std::string& s){ for(char& c: s) c=(char)std::tolower((unsigned char)c); }
203 static inline std::string lowercase(const std::string& s){ std::string t=s; tolower_inplace(t); return t; }
204};
205
206// --- Definition outside class (no default args here) ---
207inline bool Engine::load_patterns_sqlite(const std::string& db_path, const std::string& table) {
208 sqlite3* db = nullptr;
209 if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) return false;
210
211 std::string sql = "SELECT pattern, is_regex FROM " + table + ";";
212 sqlite3_stmt* stmt = nullptr;
213 if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
214 sqlite3_close(db);
215 return false;
216 }
217
218 std::vector<std::string> pats; pats.reserve(100000);
219 std::vector<std::string> regex_pats; regex_pats.reserve(10000);
220
221 while (sqlite3_step(stmt) == SQLITE_ROW) {
222 const unsigned char* p = sqlite3_column_text(stmt, 0);
223 int is_regex = sqlite3_column_int(stmt, 1);
224 if (!p) continue;
225 std::string s = reinterpret_cast<const char*>(p);
226 trim(s); tolower_inplace(s);
227 if (s.empty()) continue;
228 if (is_regex) regex_pats.push_back(s); else pats.push_back(s);
229 }
230 sqlite3_finalize(stmt);
231 sqlite3_close(db);
232
233 // build bloom + AC
234 build_bloom(pats.size());
235 for (auto& p : pats) bloom.add(p);
236 for (auto& p : pats) ac.add(p);
237 ac.build();
238 total_patterns = pats.size();
239
240#ifdef FASTFILTER_USE_RE2
241 regexes.clear();
242 for (auto& r : regex_pats) regexes.emplace_back(std::make_unique<RE2>(r));
243#endif
244
245 return true;
246}
247
248} // namespace fastfilter
249
250#ifdef FASTFILTER_IMPLEMENTATION
251// header-only: nothing separate to compile
252#endif
bool load_patterns_file(const std::string &path)
static void tolower_inplace(std::string &s)
static void trim(std::string &s)
size_t get_pattern_count() const
static std::string lowercase(const std::string &s)
Engine(const Config &c)
bool load_patterns_sqlite(const std::string &db_path, const std::string &table="phishing_rules")
bool load_regex_file(const std::string &path)
void build_bloom(size_t n)
Verdict check_url(const std::string &url)
static uint64_t rotl64(uint64_t x, int8_t r)
Definition fast_filter.h:48
static uint64_t murmur3_64(const void *key, size_t len, uint32_t seed=0xB00B1E)
Definition fast_filter.h:52
static uint64_t fmix64(uint64_t k)
Definition fast_filter.h:49
std::unordered_map< char, int > next
std::vector< ACNode > g
void add(const std::string &pat)
static char norm(char c)
bool search_any(const std::string &s) const
std::vector< uint8_t > buf
Definition fast_filter.h:88
void add(const std::string &s)
Definition fast_filter.h:99
bool might_contain(const std::string &s) const
void _setbit(uint64_t h)
Definition fast_filter.h:97
void init(size_t n_items, double fp_rate)
Definition fast_filter.h:89
bool _getbit(uint64_t h) const
Definition fast_filter.h:98
std::unordered_map< uint64_t, std::pair< bool, std::list< uint64_t >::iterator > > map
LRUCache(size_t c=100000)
bool get(uint64_t k, bool &out)
void put(uint64_t k, bool v)
std::list< uint64_t > order
enum fastfilter::Verdict::Stage stage