@hraness/kb 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
use std::alloc::{GlobalAlloc, Layout, System};
|
|
2
|
+
use std::io::{self, Read};
|
|
3
|
+
use std::net::{IpAddr, SocketAddr};
|
|
4
|
+
use std::ptr::null_mut;
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
7
|
+
use std::time::Duration;
|
|
8
|
+
|
|
9
|
+
use metadata_search_engine_rs::{
|
|
10
|
+
aggregator::aggregate,
|
|
11
|
+
engines::{
|
|
12
|
+
BraveEngine, DuckDuckGoEngine, SearchEngine, StartpageEngine, YahooEngine,
|
|
13
|
+
},
|
|
14
|
+
models::SearchResponse,
|
|
15
|
+
};
|
|
16
|
+
use reqwest::{
|
|
17
|
+
Client,
|
|
18
|
+
header::{self, HeaderMap, HeaderValue},
|
|
19
|
+
redirect::Policy,
|
|
20
|
+
};
|
|
21
|
+
use serde::Deserialize;
|
|
22
|
+
|
|
23
|
+
const MAX_INPUT_BYTES: u64 = 16 * 1024;
|
|
24
|
+
const MAX_QUERY_BYTES: usize = 4 * 1024;
|
|
25
|
+
const MAX_RESULTS: usize = 20;
|
|
26
|
+
const MIN_TIMEOUT_MS: u64 = 500;
|
|
27
|
+
const MAX_TIMEOUT_MS: u64 = 15_000;
|
|
28
|
+
const MAX_ENGINE_ADDRESSES: usize = 8;
|
|
29
|
+
#[cfg(target_os = "linux")]
|
|
30
|
+
const PROCESS_DATA_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
|
|
31
|
+
const TRACKED_HEAP_LIMIT_BYTES: usize = 128 * 1024 * 1024;
|
|
32
|
+
const ENGINE_HOSTS: [&str; 4] = [
|
|
33
|
+
"html.duckduckgo.com",
|
|
34
|
+
"search.brave.com",
|
|
35
|
+
"www.startpage.com",
|
|
36
|
+
"search.yahoo.com",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
struct BoundedAllocator;
|
|
40
|
+
|
|
41
|
+
static TRACKED_HEAP_BYTES: AtomicUsize = AtomicUsize::new(0);
|
|
42
|
+
|
|
43
|
+
fn reserve_heap(bytes: usize) -> bool {
|
|
44
|
+
let mut current = TRACKED_HEAP_BYTES.load(Ordering::Relaxed);
|
|
45
|
+
loop {
|
|
46
|
+
let Some(next) = current.checked_add(bytes) else {
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
if next > TRACKED_HEAP_LIMIT_BYTES {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
match TRACKED_HEAP_BYTES.compare_exchange_weak(
|
|
53
|
+
current,
|
|
54
|
+
next,
|
|
55
|
+
Ordering::AcqRel,
|
|
56
|
+
Ordering::Relaxed,
|
|
57
|
+
) {
|
|
58
|
+
Ok(_) => return true,
|
|
59
|
+
Err(observed) => current = observed,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn release_heap(bytes: usize) {
|
|
65
|
+
TRACKED_HEAP_BYTES.fetch_sub(bytes, Ordering::AcqRel);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Every Rust-owned response buffer, parsed DOM, and result object passes
|
|
69
|
+
// through this allocator. Native-library process data is additionally bounded
|
|
70
|
+
// through RLIMIT_DATA where the host kernel supports lowering it (Linux).
|
|
71
|
+
unsafe impl GlobalAlloc for BoundedAllocator {
|
|
72
|
+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
|
73
|
+
if !reserve_heap(layout.size()) {
|
|
74
|
+
return null_mut();
|
|
75
|
+
}
|
|
76
|
+
// SAFETY: the system allocator receives the caller's valid layout.
|
|
77
|
+
let pointer = unsafe { System.alloc(layout) };
|
|
78
|
+
if pointer.is_null() {
|
|
79
|
+
release_heap(layout.size());
|
|
80
|
+
}
|
|
81
|
+
pointer
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
|
85
|
+
if !reserve_heap(layout.size()) {
|
|
86
|
+
return null_mut();
|
|
87
|
+
}
|
|
88
|
+
// SAFETY: the system allocator receives the caller's valid layout.
|
|
89
|
+
let pointer = unsafe { System.alloc_zeroed(layout) };
|
|
90
|
+
if pointer.is_null() {
|
|
91
|
+
release_heap(layout.size());
|
|
92
|
+
}
|
|
93
|
+
pointer
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
|
|
97
|
+
// SAFETY: the pointer and layout came from this allocator.
|
|
98
|
+
unsafe { System.dealloc(pointer, layout) };
|
|
99
|
+
release_heap(layout.size());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
|
103
|
+
// Conservatively reserve the complete replacement before asking the
|
|
104
|
+
// system allocator to grow or move the old allocation.
|
|
105
|
+
if !reserve_heap(new_size) {
|
|
106
|
+
return null_mut();
|
|
107
|
+
}
|
|
108
|
+
// SAFETY: the pointer and old layout came from this allocator.
|
|
109
|
+
let replacement = unsafe { System.realloc(pointer, layout, new_size) };
|
|
110
|
+
if replacement.is_null() {
|
|
111
|
+
release_heap(new_size);
|
|
112
|
+
} else {
|
|
113
|
+
release_heap(layout.size());
|
|
114
|
+
}
|
|
115
|
+
replacement
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[global_allocator]
|
|
120
|
+
static GLOBAL_ALLOCATOR: BoundedAllocator = BoundedAllocator;
|
|
121
|
+
|
|
122
|
+
#[derive(Deserialize)]
|
|
123
|
+
#[serde(deny_unknown_fields)]
|
|
124
|
+
struct ResolvedAddress {
|
|
125
|
+
address: String,
|
|
126
|
+
family: u8,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[derive(Deserialize)]
|
|
130
|
+
#[serde(deny_unknown_fields)]
|
|
131
|
+
struct EngineHost {
|
|
132
|
+
hostname: String,
|
|
133
|
+
addresses: Vec<ResolvedAddress>,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[derive(Deserialize)]
|
|
137
|
+
#[serde(deny_unknown_fields)]
|
|
138
|
+
struct SearchRequest {
|
|
139
|
+
schema_version: u8,
|
|
140
|
+
query: String,
|
|
141
|
+
max_results: usize,
|
|
142
|
+
timeout_ms: u64,
|
|
143
|
+
engine_hosts: Vec<EngineHost>,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn read_request() -> Result<SearchRequest, &'static str> {
|
|
147
|
+
let mut bytes = Vec::new();
|
|
148
|
+
io::stdin()
|
|
149
|
+
.take(MAX_INPUT_BYTES + 1)
|
|
150
|
+
.read_to_end(&mut bytes)
|
|
151
|
+
.map_err(|_| "stdin-read")?;
|
|
152
|
+
if bytes.len() as u64 > MAX_INPUT_BYTES {
|
|
153
|
+
return Err("input-too-large");
|
|
154
|
+
}
|
|
155
|
+
let request: SearchRequest = serde_json::from_slice(&bytes).map_err(|_| "invalid-json")?;
|
|
156
|
+
if request.schema_version != 1 {
|
|
157
|
+
return Err("unsupported-schema");
|
|
158
|
+
}
|
|
159
|
+
if request.query.trim().is_empty() || request.query.as_bytes().len() > MAX_QUERY_BYTES {
|
|
160
|
+
return Err("invalid-query");
|
|
161
|
+
}
|
|
162
|
+
if request.max_results == 0 || request.max_results > MAX_RESULTS {
|
|
163
|
+
return Err("invalid-result-limit");
|
|
164
|
+
}
|
|
165
|
+
if !(MIN_TIMEOUT_MS..=MAX_TIMEOUT_MS).contains(&request.timeout_ms) {
|
|
166
|
+
return Err("invalid-timeout");
|
|
167
|
+
}
|
|
168
|
+
if request.engine_hosts.len() != ENGINE_HOSTS.len() {
|
|
169
|
+
return Err("invalid-engine-hosts");
|
|
170
|
+
}
|
|
171
|
+
for (host, expected) in request.engine_hosts.iter().zip(ENGINE_HOSTS) {
|
|
172
|
+
if host.hostname != expected
|
|
173
|
+
|| host.addresses.is_empty()
|
|
174
|
+
|| host.addresses.len() > MAX_ENGINE_ADDRESSES
|
|
175
|
+
{
|
|
176
|
+
return Err("invalid-engine-hosts");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
Ok(request)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn fail(code: &'static str) -> ! {
|
|
183
|
+
eprintln!("metadata search failed: {code}");
|
|
184
|
+
std::process::exit(2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[cfg(target_os = "linux")]
|
|
188
|
+
fn constrain_process_memory() -> Result<(), &'static str> {
|
|
189
|
+
let limit = libc::rlimit {
|
|
190
|
+
rlim_cur: PROCESS_DATA_LIMIT_BYTES as libc::rlim_t,
|
|
191
|
+
rlim_max: PROCESS_DATA_LIMIT_BYTES as libc::rlim_t,
|
|
192
|
+
};
|
|
193
|
+
// SAFETY: `limit` is fully initialized and lowering these process-local
|
|
194
|
+
// resource ceilings cannot grant privileges or affect the parent.
|
|
195
|
+
unsafe {
|
|
196
|
+
if libc::setrlimit(libc::RLIMIT_DATA, &limit) != 0 {
|
|
197
|
+
return Err("memory-limit-data");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
Ok(())
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[cfg(not(target_os = "linux"))]
|
|
204
|
+
fn constrain_process_memory() -> Result<(), &'static str> {
|
|
205
|
+
Ok(())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn is_private_or_reserved(address: IpAddr) -> bool {
|
|
209
|
+
match address {
|
|
210
|
+
IpAddr::V4(address) => {
|
|
211
|
+
let [a, b, c, _] = address.octets();
|
|
212
|
+
a == 0
|
|
213
|
+
|| a == 10
|
|
214
|
+
|| a == 127
|
|
215
|
+
|| (a == 100 && (64..=127).contains(&b))
|
|
216
|
+
|| (a == 169 && b == 254)
|
|
217
|
+
|| (a == 172 && (16..=31).contains(&b))
|
|
218
|
+
|| (a == 192 && (b == 0 || b == 168))
|
|
219
|
+
|| (a == 192 && b == 0 && c == 2)
|
|
220
|
+
|| (a == 198 && (b == 18 || b == 19))
|
|
221
|
+
|| (a == 198 && b == 51 && c == 100)
|
|
222
|
+
|| (a == 203 && b == 0 && c == 113)
|
|
223
|
+
|| a >= 224
|
|
224
|
+
}
|
|
225
|
+
IpAddr::V6(address) => {
|
|
226
|
+
let groups = address.segments();
|
|
227
|
+
let ipv4_compatible = groups[..6].iter().all(|group| *group == 0);
|
|
228
|
+
let ipv4_mapped = groups[..5].iter().all(|group| *group == 0)
|
|
229
|
+
&& groups[5] == 0xffff;
|
|
230
|
+
if ipv4_compatible || ipv4_mapped {
|
|
231
|
+
let high = groups[6];
|
|
232
|
+
let low = groups[7];
|
|
233
|
+
let embedded = std::net::Ipv4Addr::new(
|
|
234
|
+
(high >> 8) as u8,
|
|
235
|
+
high as u8,
|
|
236
|
+
(low >> 8) as u8,
|
|
237
|
+
low as u8,
|
|
238
|
+
);
|
|
239
|
+
return is_private_or_reserved(IpAddr::V4(embedded));
|
|
240
|
+
}
|
|
241
|
+
let first = groups[0];
|
|
242
|
+
let second = groups[1];
|
|
243
|
+
(first & 0xfe00) == 0xfc00
|
|
244
|
+
|| (first & 0xffc0) == 0xfe80
|
|
245
|
+
|| (first & 0xffc0) == 0xfec0
|
|
246
|
+
|| (first & 0xff00) == 0xff00
|
|
247
|
+
|| first == 0x0064
|
|
248
|
+
|| first == 0x0100
|
|
249
|
+
|| (first == 0x2001 && second <= 0x01ff)
|
|
250
|
+
|| (first == 0x2001 && second == 0x0db8)
|
|
251
|
+
|| first == 0x2002
|
|
252
|
+
|| first == 0x3ffe
|
|
253
|
+
|| (first == 0x3fff && (second & 0xf000) == 0)
|
|
254
|
+
|| first == 0x5f00
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn global_socket_addresses(host: &EngineHost) -> Result<Vec<SocketAddr>, &'static str> {
|
|
260
|
+
let mut addresses = Vec::with_capacity(host.addresses.len());
|
|
261
|
+
for input in &host.addresses {
|
|
262
|
+
let address = input
|
|
263
|
+
.address
|
|
264
|
+
.parse::<IpAddr>()
|
|
265
|
+
.map_err(|_| "invalid-engine-address")?;
|
|
266
|
+
let family = if address.is_ipv4() { 4 } else { 6 };
|
|
267
|
+
if family != input.family || is_private_or_reserved(address) {
|
|
268
|
+
return Err("invalid-engine-address");
|
|
269
|
+
}
|
|
270
|
+
let socket = SocketAddr::new(address, 443);
|
|
271
|
+
if addresses.contains(&socket) {
|
|
272
|
+
return Err("invalid-engine-address");
|
|
273
|
+
}
|
|
274
|
+
addresses.push(socket);
|
|
275
|
+
}
|
|
276
|
+
Ok(addresses)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn build_pinned_http_client(
|
|
280
|
+
hosts: &[EngineHost],
|
|
281
|
+
timeout: Duration,
|
|
282
|
+
) -> Result<Client, &'static str> {
|
|
283
|
+
let mut headers = HeaderMap::new();
|
|
284
|
+
headers.insert(
|
|
285
|
+
header::USER_AGENT,
|
|
286
|
+
HeaderValue::from_static(
|
|
287
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
|
|
288
|
+
AppleWebKit/537.36 (KHTML, like Gecko) \
|
|
289
|
+
Chrome/124.0.0.0 Safari/537.36",
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
headers.insert(
|
|
293
|
+
header::ACCEPT,
|
|
294
|
+
HeaderValue::from_static(
|
|
295
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
headers.insert(
|
|
299
|
+
header::ACCEPT_LANGUAGE,
|
|
300
|
+
HeaderValue::from_static("en-US,en;q=0.9"),
|
|
301
|
+
);
|
|
302
|
+
headers.insert(
|
|
303
|
+
header::ACCEPT_ENCODING,
|
|
304
|
+
HeaderValue::from_static("gzip, deflate, br"),
|
|
305
|
+
);
|
|
306
|
+
headers.insert("DNT", HeaderValue::from_static("1"));
|
|
307
|
+
headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("document"));
|
|
308
|
+
headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("navigate"));
|
|
309
|
+
headers.insert("Sec-Fetch-Site", HeaderValue::from_static("none"));
|
|
310
|
+
headers.insert("Sec-Fetch-User", HeaderValue::from_static("?1"));
|
|
311
|
+
|
|
312
|
+
let mut builder = Client::builder()
|
|
313
|
+
.default_headers(headers)
|
|
314
|
+
.cookie_store(true)
|
|
315
|
+
.gzip(true)
|
|
316
|
+
.brotli(true)
|
|
317
|
+
.redirect(Policy::none())
|
|
318
|
+
.timeout(timeout);
|
|
319
|
+
for host in hosts {
|
|
320
|
+
let addresses = global_socket_addresses(host)?;
|
|
321
|
+
builder = builder.resolve_to_addrs(&host.hostname, &addresses);
|
|
322
|
+
}
|
|
323
|
+
builder.build().map_err(|_| "http-client")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn main() {
|
|
327
|
+
if let Err(code) = constrain_process_memory() {
|
|
328
|
+
fail(code);
|
|
329
|
+
}
|
|
330
|
+
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
331
|
+
.enable_all()
|
|
332
|
+
.build()
|
|
333
|
+
.unwrap_or_else(|_| fail("runtime"));
|
|
334
|
+
runtime.block_on(run());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async fn run() {
|
|
338
|
+
let request = match read_request() {
|
|
339
|
+
Ok(request) => request,
|
|
340
|
+
Err(code) => fail(code),
|
|
341
|
+
};
|
|
342
|
+
let timeout = Duration::from_millis(request.timeout_ms / ENGINE_HOSTS.len() as u64);
|
|
343
|
+
let client = Arc::new(
|
|
344
|
+
build_pinned_http_client(&request.engine_hosts, timeout)
|
|
345
|
+
.unwrap_or_else(|code| fail(code)),
|
|
346
|
+
);
|
|
347
|
+
let engines: Vec<Arc<dyn SearchEngine>> = vec![
|
|
348
|
+
Arc::new(DuckDuckGoEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
349
|
+
Arc::new(BraveEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
350
|
+
Arc::new(StartpageEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
351
|
+
Arc::new(YahooEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
352
|
+
];
|
|
353
|
+
let engines_queried = engines
|
|
354
|
+
.iter()
|
|
355
|
+
.map(|engine| engine.name().to_string())
|
|
356
|
+
.collect::<Vec<_>>();
|
|
357
|
+
let mut successes = Vec::new();
|
|
358
|
+
let mut engines_failed = Vec::new();
|
|
359
|
+
// Run one upstream engine at a time so its unbounded internal `text()`
|
|
360
|
+
// allocation is contained by the process memory ceiling rather than
|
|
361
|
+
// multiplied across four concurrent responses.
|
|
362
|
+
for engine in &engines {
|
|
363
|
+
let name = engine.name().to_string();
|
|
364
|
+
match engine
|
|
365
|
+
.search(request.query.trim(), request.max_results)
|
|
366
|
+
.await
|
|
367
|
+
{
|
|
368
|
+
Ok(results) => successes.push((name, results)),
|
|
369
|
+
Err(_) => engines_failed.push(name),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
let mut results = aggregate(successes, request.max_results);
|
|
373
|
+
results.sort_by(|left, right| {
|
|
374
|
+
right
|
|
375
|
+
.score
|
|
376
|
+
.total_cmp(&left.score)
|
|
377
|
+
.then_with(|| left.title.cmp(&right.title))
|
|
378
|
+
.then_with(|| left.url.cmp(&right.url))
|
|
379
|
+
});
|
|
380
|
+
let response = SearchResponse {
|
|
381
|
+
query: request.query.trim().to_string(),
|
|
382
|
+
results,
|
|
383
|
+
engines_queried,
|
|
384
|
+
engines_failed,
|
|
385
|
+
};
|
|
386
|
+
if serde_json::to_writer(io::stdout().lock(), &response).is_err() {
|
|
387
|
+
fail("stdout-write");
|
|
388
|
+
}
|
|
389
|
+
}
|