1mod resolution;
4#[cfg(test)]
5mod tests;
6
7use mant_ir::{
8 ContentRevealRef, Document, DocumentAddress, LinkOccurrenceRef, LinkTarget, NavigationEvent,
9 NavigationScanOptions, ReferenceFormAssociationState, ReferenceLinkFilter, ReferenceScanLimits,
10 ReferenceScope, ReferenceTargetType, ReferenceWorkBudget, reference_form_associations,
11 scan_navigation_scope_with_budget,
12};
13use mant_protocol::{
14 ReferenceAssociation, ReferenceCount, ReferenceCoverage, ReferenceInventory,
15 ReferencePageLimit, ReferenceProjection, ReferenceProjectionMode, ReferenceRecord,
16 ReferenceUnknownReason,
17};
18use std::{collections::BTreeSet, ops::ControlFlow};
19
20#[derive(Debug, Clone, Copy)]
22pub struct ReferenceProjectionLimits {
23 pub scan: ReferenceScanLimits,
25 pub distinct_targets: usize,
27 pub distinct_bytes: usize,
29 pub materialization_bytes: usize,
31}
32
33impl Default for ReferenceProjectionLimits {
34 fn default() -> Self {
35 Self {
36 scan: ReferenceScanLimits::default(),
37 distinct_targets: 4096,
38 distinct_bytes: 1024 * 1024,
39 materialization_bytes: 256 * 1024,
40 }
41 }
42}
43
44#[must_use]
47pub fn project_references(
48 document: &Document,
49 source_address: Option<&DocumentAddress>,
50 scope: ReferenceScope<'_>,
51 policy: &ReferenceProjection,
52) -> ReferenceInventory {
53 project_references_with_limits(
54 document,
55 source_address,
56 scope,
57 policy,
58 ReferenceProjectionLimits::default(),
59 )
60}
61
62#[must_use]
64pub fn project_references_with_limits(
65 document: &Document,
66 source_address: Option<&DocumentAddress>,
67 scope: ReferenceScope<'_>,
68 policy: &ReferenceProjection,
69 mut limits: ReferenceProjectionLimits,
70) -> ReferenceInventory {
71 limits.distinct_targets = limits.distinct_targets.min(4096);
72 limits.distinct_bytes = limits.distinct_bytes.min(1024 * 1024);
73 limits.materialization_bytes = limits.materialization_bytes.min(1024 * 1024);
74 if policy.validate().is_err() {
75 return invalid_policy(policy);
76 }
77 let mut result = ReferenceInventory::not_scanned(policy.clone());
78 if policy.mode == ReferenceProjectionMode::None {
79 return result;
80 }
81 let mut budget = ReferenceWorkBudget::new(limits.scan);
82 let mut targets = BTreeSet::new();
83 let mut target_bytes = 0usize;
84 let mut targets_complete = true;
85 let mut occurrences = 0usize;
86 let mut retained_bytes = 0usize;
87 let source_bytes = source_address.map_or(0, address_bytes);
88 let report = scan_navigation_scope_with_budget(
89 document,
90 scope,
91 &mut budget,
92 NavigationScanOptions {
93 links: ReferenceLinkFilter::from_types(&policy.target_types),
94 ..Default::default()
95 },
96 |event, budget| {
97 let NavigationEvent::Link(occurrence) = event else {
98 return ControlFlow::Continue(());
99 };
100 let index = occurrences;
101 occurrences += 1;
102 if targets_complete {
103 if budget
106 .consume(
107 occurrence.location.depth(),
108 1,
109 target_size(occurrence.target).saturating_mul(16),
110 )
111 .is_err()
112 {
113 return ControlFlow::Break(());
114 }
115 let key = target_key(occurrence.target);
116 if !targets.contains(&key) {
117 let bytes = target_size(occurrence.target).saturating_add(32);
118 if targets.len() >= limits.distinct_targets
119 || bytes > limits.distinct_bytes.saturating_sub(target_bytes)
120 {
121 targets_complete = false;
122 } else {
123 targets.insert(key);
124 target_bytes += bytes;
125 }
126 }
127 }
128 if policy.mode != ReferenceProjectionMode::All
129 || index < (policy.offset as usize)
130 || result.page.limited.is_some()
131 {
132 return ControlFlow::Continue(());
133 }
134 if result.records.len() >= policy.limit as usize {
135 result.page.limited = Some(ReferencePageLimit::Records);
136 return ControlFlow::Continue(());
137 }
138 match materialize(
139 occurrence,
140 source_address,
141 source_bytes,
142 budget,
143 &mut retained_bytes,
144 limits.materialization_bytes,
145 ) {
146 Ok(record) => result.records.push(record),
147 Err(limit) => result.page.limited = Some(limit),
148 }
149 ControlFlow::Continue(())
150 },
151 );
152 finish_inventory(
153 &mut result,
154 report,
155 occurrences,
156 targets.len(),
157 targets_complete,
158 );
159 result.target_coverage = resolution::validate_local(
160 document,
161 source_address,
162 &mut result.records,
163 &mut budget,
164 &mut retained_bytes,
165 limits.materialization_bytes,
166 );
167 result
168}
169
170fn invalid_policy(policy: &ReferenceProjection) -> ReferenceInventory {
171 let mut invalid = ReferenceInventory::not_scanned(ReferenceProjection {
173 mode: policy.mode,
174 target_types: Vec::new(),
175 offset: policy.offset,
176 limit: policy.limit.clamp(1, 1000),
177 });
178 invalid.occurrences = ReferenceCount::Unknown {
179 reason: ReferenceUnknownReason::InvalidPolicy,
180 };
181 invalid.targets = ReferenceCount::Unknown {
182 reason: ReferenceUnknownReason::InvalidPolicy,
183 };
184 invalid
185}
186
187fn finish_inventory(
188 result: &mut ReferenceInventory,
189 report: mant_ir::ReferenceScanReport,
190 occurrences: usize,
191 targets: usize,
192 targets_complete: bool,
193) {
194 let count = |value, exact| {
195 if exact {
196 ReferenceCount::Exact { value }
197 } else {
198 ReferenceCount::LowerBound { value }
199 }
200 };
201 result.occurrences = count(occurrences as u64, report.complete());
202 result.targets = count(targets as u64, report.complete() && targets_complete);
203 result.coverage = ReferenceCoverage::from_report(report);
204 result.page.returned = u32::try_from(result.records.len()).unwrap_or(u32::MAX);
205 if !report.complete()
206 && result.policy.mode == ReferenceProjectionMode::All
207 && result.page.limited.is_none()
208 {
209 result.page.limited = Some(ReferencePageLimit::Scan);
210 }
211 let next = result.policy.offset.saturating_add(result.page.returned);
212 if report.complete() && result.page.returned > 0 && (next as usize) < occurrences {
215 result.page.next_offset = Some(next);
216 }
217}
218
219fn materialize(
220 occurrence: LinkOccurrenceRef<'_, '_>,
221 source_address: Option<&DocumentAddress>,
222 source_bytes: usize,
223 budget: &mut ReferenceWorkBudget,
224 retained: &mut usize,
225 limit: usize,
226) -> Result<ReferenceRecord, ReferencePageLimit> {
227 let owner = occurrence
228 .content_owner
229 .map(|owner| ContentRevealRef::Owner(owner.location));
230 let semantic_owner = occurrence
231 .semantic_owner
232 .map(|owner| ContentRevealRef::Owner(owner.location));
233 budget
234 .consume(
235 occurrence.location.depth(),
236 occurrence
237 .location
238 .depth()
239 .saturating_add(owner.map_or(0, ContentRevealRef::depth))
240 .saturating_add(semantic_owner.map_or(0, ContentRevealRef::depth))
241 .saturating_mul(4),
242 source_bytes,
243 )
244 .map_err(|_| ReferencePageLimit::Scan)?;
245 let owner_bytes = owner.map_or(0, ContentRevealRef::encoded_size_bound);
246 let forms_reservation = occurrence
247 .semantic_owner
248 .and_then(|owner| owner.owner.facts())
249 .map_or(0, |facts| {
250 facts
251 .forms
252 .len()
253 .min(mant_ir::MAX_REFERENCE_FORM_ASSOCIATIONS)
254 .saturating_mul(12)
255 });
256 let bytes = occurrence
257 .location
258 .encoded_len()
259 .saturating_mul(2)
260 .saturating_add(owner_bytes)
261 .saturating_add(semantic_owner.map_or(0, ContentRevealRef::encoded_size_bound))
262 .saturating_add(target_size(occurrence.target).saturating_mul(3))
263 .saturating_add(source_bytes.saturating_mul(2))
264 .saturating_add(forms_reservation)
265 .saturating_add(256);
266 if bytes > limit.saturating_sub(*retained) {
267 return Err(ReferencePageLimit::MaterializationBytes);
268 }
269 let origin = occurrence
270 .location
271 .to_owned()
272 .ok_or(ReferencePageLimit::Position)?;
273 let owner = match owner {
274 Some(owner) => Some(owner.to_owned().ok_or(ReferencePageLimit::Position)?),
275 None => None,
276 };
277 let label = mant_ir::reference_label(
278 occurrence.label,
279 occurrence.location.depth(),
280 budget,
281 4096.min(limit.saturating_sub(*retained).saturating_sub(bytes)),
282 )
283 .map_err(|_| ReferencePageLimit::Scan)?;
284 let label_truncated = label.truncated;
285 let label = label.text;
286 let association = reference_form_associations(occurrence, budget);
287 let association = match association.state {
288 ReferenceFormAssociationState::Unrecorded => ReferenceAssociation::Unrecorded {},
289 ReferenceFormAssociationState::Complete => ReferenceAssociation::Valid {
290 forms: association.forms,
291 owner: semantic_owner
292 .and_then(ContentRevealRef::to_owned)
293 .ok_or(ReferencePageLimit::Position)?,
294 },
295 ReferenceFormAssociationState::Invalid => ReferenceAssociation::Invalid {},
296 ReferenceFormAssociationState::Limited(_)
297 | ReferenceFormAssociationState::MaterializationLimit => ReferenceAssociation::Limited {},
298 };
299 let total = bytes.saturating_add(label.len());
300 if total > limit.saturating_sub(*retained) {
301 return Err(ReferencePageLimit::MaterializationBytes);
302 }
303 *retained += total;
304 Ok(ReferenceRecord {
305 source_read: source_read(occurrence.location),
306 origin,
307 owner,
308 label,
309 label_truncated,
310 target: occurrence.target.clone(),
311 association,
312 resolution: resolution::initial(occurrence.target, source_address),
313 })
314}
315
316fn source_read(location: mant_ir::ContentLocationRef<'_>) -> mant_protocol::ContentSelector {
317 use std::fmt::Write;
318 let sections = match location {
319 mant_ir::ContentLocationRef::DocumentHeading { .. } => &[][..],
320 mant_ir::ContentLocationRef::SectionHeading { sections, .. }
321 | mant_ir::ContentLocationRef::Content { sections, .. } => sections,
322 };
323 let mut path = String::new();
324 for index in sections {
325 let number = u64::from(*index) + 1;
326 let digits = number.ilog10() as usize + 1;
327 if path.len() + usize::from(!path.is_empty()) + digits > 512 {
330 break;
331 }
332 if !path.is_empty() {
333 path.push('.');
334 }
335 write!(&mut path, "{number}").expect("String write cannot fail");
336 }
337 mant_protocol::ContentSelector::path(if path.is_empty() {
338 "root".to_owned()
339 } else {
340 path
341 })
342}
343
344fn target_key(target: &LinkTarget) -> (ReferenceTargetType, &str, Option<&str>) {
345 match target {
346 LinkTarget::Document { name, fragment } => {
347 (ReferenceTargetType::Document, name, fragment.as_deref())
348 }
349 LinkTarget::Manual {
350 name,
351 manual_section,
352 } => (ReferenceTargetType::Manual, name, manual_section.as_deref()),
353 LinkTarget::Section { id } => (ReferenceTargetType::Local, id.as_str(), None),
354 LinkTarget::External { uri } => (ReferenceTargetType::External, uri, None),
355 LinkTarget::Email { address } => (ReferenceTargetType::Email, address, None),
356 }
357}
358
359fn target_size(target: &LinkTarget) -> usize {
360 let (_, a, b) = target_key(target);
361 a.len().saturating_add(b.map_or(0, str::len))
362}
363fn address_bytes(address: &DocumentAddress) -> usize {
364 match address {
365 DocumentAddress::Manual {
366 name,
367 manual_section,
368 } => name.len().saturating_add(manual_section.len()),
369 DocumentAddress::Markdown { path, origin } => path.len().saturating_add(match origin {
370 mant_ir::MarkdownOrigin::Documents => 0,
371 mant_ir::MarkdownOrigin::Source { name } => name.len(),
372 }),
373 }
374}