1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
//! What a run reports: the typed outcomes a missing or superseded archive
//! produces, and the rows and references a successful one does.
use super::{
ArchivePropertyDisplay, Error, PropertyType, RecordIdentifier, fmt, oak_property_type_name,
};
/// Failure from archive path attribution.
#[derive(Debug)]
#[non_exhaustive]
pub enum ArchiveDebugError {
/// Reading or interpreting repository content failed.
Repository(Error),
/// Retaining another result row would exceed a configured count or text
/// budget.
ResultBudgetExceeded {
/// Configured maximum result rows.
maximum_path_references: usize,
/// Configured maximum cloned UTF-8 bytes.
maximum_reference_text_bytes: usize,
/// Row count that the rejected insertion would have reached.
attempted_path_references: usize,
/// Cloned text bytes that the rejected insertion would have reached.
attempted_reference_text_bytes: usize,
},
/// Performing the next logical record-graph operation would exceed the
/// configured total-work budget.
WorkBudgetExceeded {
/// Configured maximum logical work units.
maximum_work_units: u64,
/// Work total the rejected operation would have reached.
attempted_work_units: u64,
},
/// One node declares more children than the configured per-node
/// materialization cap.
NodeChildBudgetExceeded {
/// Configured maximum child entries materialized for one node.
maximum_scheduled_children_per_node: u64,
/// Child count read before allocating the entry vector.
attempted_scheduled_children: u64,
},
/// One node's child and template names exceed the configured
/// materialization cap.
NodeNameBudgetExceeded {
/// Configured maximum cumulative stored name bytes for one node.
maximum_name_bytes_per_node: u64,
/// Cumulative stored bytes including the rejected name.
attempted_name_bytes: u64,
},
/// Expanding one node would retain more pending child visits than the
/// configured traversal-stack cap.
PendingNodeBudgetExceeded {
/// Configured maximum pending child visits.
maximum_pending_nodes: u64,
/// Pending visits after the rejected expansion.
attempted_pending_nodes: u64,
},
/// Parsing or totalizing the archive graph would exceed its configured
/// row or edge cap.
GraphBudgetExceeded {
/// Configured maximum graph rows.
maximum_graph_rows: usize,
/// Configured maximum graph edges.
maximum_graph_edges: usize,
/// Rows after the rejected operation.
attempted_graph_rows: usize,
/// Edges after the rejected operation.
attempted_graph_edges: usize,
},
}
impl fmt::Display for ArchiveDebugError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Repository(source) => source.fmt(formatter),
Self::ResultBudgetExceeded {
maximum_path_references,
maximum_reference_text_bytes,
attempted_path_references,
attempted_reference_text_bytes,
} => write!(
formatter,
"archive attribution result would retain {attempted_path_references} rows and \
{attempted_reference_text_bytes} text bytes, exceeding limits of \
{maximum_path_references} rows and {maximum_reference_text_bytes} text bytes"
),
Self::WorkBudgetExceeded {
maximum_work_units,
attempted_work_units,
} => write!(
formatter,
"archive attribution would perform {attempted_work_units} logical work units, \
exceeding the limit of {maximum_work_units}"
),
Self::NodeChildBudgetExceeded {
maximum_scheduled_children_per_node,
attempted_scheduled_children,
} => write!(
formatter,
"archive attribution node declares {attempted_scheduled_children} children, \
exceeding the per-node limit of {maximum_scheduled_children_per_node}"
),
Self::NodeNameBudgetExceeded {
maximum_name_bytes_per_node,
attempted_name_bytes,
} => write!(
formatter,
"archive attribution node would materialize {attempted_name_bytes} stored name \
bytes, exceeding the per-node limit of {maximum_name_bytes_per_node}"
),
Self::PendingNodeBudgetExceeded {
maximum_pending_nodes,
attempted_pending_nodes,
} => write!(
formatter,
"archive attribution would retain {attempted_pending_nodes} pending node visits, \
exceeding the limit of {maximum_pending_nodes}"
),
Self::GraphBudgetExceeded {
maximum_graph_rows,
maximum_graph_edges,
attempted_graph_rows,
attempted_graph_edges,
} => write!(
formatter,
"archive graph would process {attempted_graph_rows} rows and \
{attempted_graph_edges} edges, exceeding limits of {maximum_graph_rows} rows \
and {maximum_graph_edges} edges"
),
}
}
}
impl std::error::Error for ArchiveDebugError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Repository(source) => Some(source),
Self::ResultBudgetExceeded { .. }
| Self::WorkBudgetExceeded { .. }
| Self::NodeChildBudgetExceeded { .. }
| Self::NodeNameBudgetExceeded { .. }
| Self::PendingNodeBudgetExceeded { .. }
| Self::GraphBudgetExceeded { .. } => None,
}
}
}
impl From<Error> for ArchiveDebugError {
fn from(source: Error) -> Self {
Self::Repository(source)
}
}
impl From<ArchiveDebugError> for Error {
fn from(source: ArchiveDebugError) -> Self {
match source {
ArchiveDebugError::Repository(source) => source,
source @ (ArchiveDebugError::ResultBudgetExceeded { .. }
| ArchiveDebugError::WorkBudgetExceeded { .. }
| ArchiveDebugError::NodeChildBudgetExceeded { .. }
| ArchiveDebugError::NodeNameBudgetExceeded { .. }
| ArchiveDebugError::PendingNodeBudgetExceeded { .. }
| ArchiveDebugError::GraphBudgetExceeded { .. }) => Error::InvalidFormat {
details: source.to_string(),
},
}
}
}
/// Result type returned by archive attribution.
pub type ArchiveDebugResult<Value> = std::result::Result<Value, ArchiveDebugError>;
/// Whether the requested file participates in the repository's active
/// read-only archive set.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum ArchiveDebugState {
/// No file with the requested archive name exists in the store.
Missing,
/// The file exists but was superseded by another generation or could
/// not be opened as the active reader for its archive number.
Inactive,
/// The file is one of the archives the repository currently reads.
Active,
}
/// Deterministic work counters for the attribution scan.
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct ArchiveDebugWork {
/// Logical work units consumed under
/// [`crate::tooling::archive_debug::options::ArchiveDebugOptions::maximum_work_units`].
pub consumed_work_units: u64,
/// Content paths visited from the super-root.
pub visited_nodes: u64,
/// Stored property slots inspected.
pub inspected_properties: u64,
/// Long-binary block identifiers inspected. This counts every block,
/// whether or not it belongs to the requested archive.
pub inspected_binary_blocks: u64,
/// Path-attribution rows retained in the report.
pub retained_path_references: u64,
/// UTF-8 bytes cloned into retained paths, names, and rendered values.
pub retained_reference_text_bytes: u64,
}
/// A current-head record whose content is attributable to an archive.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum ArchivePathReference {
/// The node record itself lives in the archive.
Node {
/// Oak-style path from the super-root, ending in `/`.
path: String,
/// The matching node record.
record_identifier: RecordIdentifier,
},
/// The node's template record lives in the archive.
Template {
/// Oak-style path from the super-root, ending in `/`.
path: String,
/// The matching template record.
record_identifier: RecordIdentifier,
},
/// A stored property's value/list record, or one or more long-binary
/// block segments, lives in the archive.
Property {
/// Oak-style path from the super-root, ending in `/`.
path: String,
/// Property name.
name: String,
/// JCR property type from the template.
property_type: PropertyType,
/// Whether the property holds an array of values.
is_multiple: bool,
/// The single value record or multi-value counted-list record Oak
/// uses as the `SegmentPropertyState` identity.
record_identifier: RecordIdentifier,
/// Whether that property identity record itself is in the archive.
record_is_in_archive: bool,
/// Oak-style value presentation, bounded for hostile exceptionally
/// large strings and non-binary arrays as documented by this module.
display: ArchivePropertyDisplay,
},
}
impl ArchivePathReference {
pub(crate) fn retained_text_bytes(&self) -> usize {
match self {
Self::Node { path, .. } | Self::Template { path, .. } => path.len(),
Self::Property {
path,
name,
display,
..
} => {
let display_bytes = display.oak_rendered_value_bytes();
path.len()
.saturating_add(name.len())
.saturating_add(display_bytes)
}
}
}
pub(crate) fn oak_rendered_utf16_sort_key(&self) -> Vec<u16> {
// DebugTars inserts the complete rendered line into a Java TreeSet.
// Names alone are not a sufficient key: adversarial property names
// can share the node/template punctuation prefixes, and Java orders
// the resulting UTF-16 code units rather than Rust scalar values.
self.oak_rendered_line().encode_utf16().collect()
}
pub(crate) fn oak_rendered_line_byte_len(&self) -> usize {
// Preflight the UTF-16 sort-key allocation by its UTF-8 source size.
// A rendered line cannot contain more UTF-16 units than UTF-8 bytes.
match self {
Self::Node {
path,
record_identifier,
} => path
.len()
.saturating_add(" [SegmentNodeState@".len())
.saturating_add(record_identifier.to_string().len())
.saturating_add(1),
Self::Template {
path,
record_identifier,
} => path
.len()
.saturating_add("[Template@".len())
.saturating_add(record_identifier.to_string().len())
.saturating_add(1),
Self::Property {
path,
name,
property_type,
is_multiple,
record_identifier,
display,
..
} => path
.len()
.saturating_add(name.len())
.saturating_add(" = ".len())
.saturating_add(display.oak_rendered_value_bytes())
.saturating_add(" [SegmentPropertyState<".len())
.saturating_add(oak_property_type_name(*property_type, *is_multiple).len())
.saturating_add(1)
.saturating_add(record_identifier.to_string().len())
.saturating_add(1),
}
}
pub(crate) fn oak_rendered_line(&self) -> String {
match self {
Self::Node {
path,
record_identifier,
} => format!("{path} [SegmentNodeState@{record_identifier}]"),
Self::Template {
path,
record_identifier,
} => format!("{path}[Template@{record_identifier}]"),
Self::Property {
path,
name,
property_type,
is_multiple,
record_identifier,
display,
..
} => {
let display = display.oak_rendered_value();
format!(
"{path}{name} = {display} [SegmentPropertyState<{}>@{record_identifier}]",
oak_property_type_name(*property_type, *is_multiple)
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::ArchivePathReference;
use crate::segment::identifier::SegmentIdentifier;
use crate::segment::record::RecordIdentifier;
use crate::tooling::archive_debug::PropertyType;
use crate::tooling::archive_debug::display::ArchivePropertyDisplay;
#[test]
fn per_node_order_uses_the_complete_rendered_java_string() {
let record_identifier =
RecordIdentifier::new(SegmentIdentifier::new(1, 0xa000_0000_0000_0001), 7);
let property = |name: &str| ArchivePathReference::Property {
path: "/".to_owned(),
name: name.to_owned(),
property_type: PropertyType::Long,
is_multiple: false,
record_identifier,
record_is_in_archive: true,
display: ArchivePropertyDisplay::Other("1".to_owned()),
};
let mut references = [
property("\u{e000}"),
ArchivePathReference::Template {
path: "/".to_owned(),
record_identifier,
},
property("["),
property("\u{10000}"),
ArchivePathReference::Node {
path: "/".to_owned(),
record_identifier,
},
property(" "),
];
references.sort_by_cached_key(ArchivePathReference::oak_rendered_utf16_sort_key);
let labels: Vec<&str> = references
.iter()
.map(|reference| match reference {
ArchivePathReference::Node { .. } => "node",
ArchivePathReference::Template { .. } => "template",
ArchivePathReference::Property { name, .. } => name,
})
.collect();
assert_eq!(
labels,
[" ", "node", "[", "template", "\u{10000}", "\u{e000}"]
);
}
}