1use super::codec::{CanonicalDecodeError, DecodeLimits, Decoder, Encoder};
4use super::codec::{DecodedPageKind, DecodedPageShape};
5use super::file_table_mutation::FileTableFormat;
6use super::frontier;
7use super::persistent_batch;
8use super::types::{FileKind, digest_object};
9use crate::async_storage::AsyncObjectStore;
10use crate::cancellation::CancellationToken;
11use crate::foundation::FileId;
12use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
13use crate::storage::{ObjectId, ObjectKind, ObjectRead, ObjectReceipt, object_digest};
14use crate::storage::{ObjectStore, ObjectStoreError};
15use std::collections::HashSet;
16use std::fmt;
17use thiserror::Error;
18
19const DOMAIN: &[u8; 8] = b"ACYFSFIL";
20const VERSION: u16 = 2;
21const RECORD_DOMAIN: &[u8; 8] = b"ACYFSREC";
22const RECORD_VERSION: u16 = 1;
23
24pub const MAXIMUM_INLINE_FILE_BYTES: usize = 64;
29
30#[derive(Clone, Copy, Eq, PartialEq)]
32pub struct InlineFileData {
33 length: u8,
34 bytes: [u8; MAXIMUM_INLINE_FILE_BYTES],
35}
36
37impl InlineFileData {
38 pub fn new(bytes: &[u8]) -> Result<Self, InlineFileDataError> {
45 let length = u8::try_from(bytes.len()).map_err(|_| InlineFileDataError::TooLarge)?;
46 if bytes.len() > MAXIMUM_INLINE_FILE_BYTES {
47 return Err(InlineFileDataError::TooLarge);
48 }
49 let mut stored = [0_u8; MAXIMUM_INLINE_FILE_BYTES];
50 #[allow(
51 clippy::indexing_slicing,
52 reason = "bytes.len() <= MAXIMUM_INLINE_FILE_BYTES == stored.len() was just checked above (the `if bytes.len() > MAXIMUM_INLINE_FILE_BYTES` early return)"
53 )]
54 stored[..bytes.len()].copy_from_slice(bytes);
55 Ok(Self {
56 length,
57 bytes: stored,
58 })
59 }
60
61 #[must_use]
63 #[allow(
64 clippy::indexing_slicing,
65 reason = "self.length <= MAXIMUM_INLINE_FILE_BYTES == self.bytes.len() is the InlineFileData invariant maintained by every constructor: new() checks bytes.len() <= MAXIMUM_INLINE_FILE_BYTES before assigning length, and replace_range/truncate check logical_bytes against the same bound before assigning length"
66 )]
67 pub fn as_bytes(&self) -> &[u8] {
68 &self.bytes[..usize::from(self.length)]
69 }
70
71 pub(crate) fn replace_range(
72 mut self,
73 offset: usize,
74 replacement: &[u8],
75 logical_bytes: usize,
76 ) -> Result<Self, InlineFileDataError> {
77 let end = offset
78 .checked_add(replacement.len())
79 .ok_or(InlineFileDataError::TooLarge)?;
80 if end > logical_bytes || logical_bytes > MAXIMUM_INLINE_FILE_BYTES {
81 return Err(InlineFileDataError::TooLarge);
82 }
83 #[allow(
84 clippy::indexing_slicing,
85 reason = "end <= logical_bytes <= MAXIMUM_INLINE_FILE_BYTES == self.bytes.len() and offset <= end (end = offset + replacement.len() via checked_add) are both established by the check just above"
86 )]
87 self.bytes[offset..end].copy_from_slice(replacement);
88 #[allow(
89 clippy::indexing_slicing,
90 reason = "logical_bytes <= MAXIMUM_INLINE_FILE_BYTES == self.bytes.len() is established by the check just above"
91 )]
92 self.bytes[logical_bytes..].fill(0);
93 self.length = u8::try_from(logical_bytes).map_err(|_| InlineFileDataError::TooLarge)?;
94 Ok(self)
95 }
96
97 pub(crate) fn truncate(mut self, logical_bytes: usize) -> Result<Self, InlineFileDataError> {
98 if logical_bytes > self.as_bytes().len() {
99 return Err(InlineFileDataError::TooLarge);
100 }
101 #[allow(
102 clippy::indexing_slicing,
103 reason = "logical_bytes <= self.as_bytes().len() <= self.bytes.len() is established by the check just above (as_bytes() never exceeds the fixed-capacity buffer)"
104 )]
105 self.bytes[logical_bytes..].fill(0);
106 self.length = u8::try_from(logical_bytes).map_err(|_| InlineFileDataError::TooLarge)?;
107 Ok(self)
108 }
109}
110
111impl fmt::Debug for InlineFileData {
112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113 formatter
114 .debug_tuple("InlineFileData")
115 .field(&self.as_bytes())
116 .finish()
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
122pub enum InlineFileDataError {
123 #[error("inline regular-file payload exceeds its canonical bound")]
125 TooLarge,
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub enum FilePayload {
131 InlineRegular(InlineFileData),
133 Regular {
135 logical_bytes: u64,
137 extents: ObjectId,
139 },
140 Directory {
142 entries: ObjectId,
144 },
145 SymbolicLink {
147 target_bytes: u64,
149 target: ObjectId,
151 },
152 Empty,
154 Device {
156 major: u32,
158 minor: u32,
160 },
161 ReparsePoint {
163 payload_bytes: u64,
165 payload: ObjectId,
167 },
168}
169
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub struct FileRecord {
173 pub file_id: FileId,
175 pub kind: FileKind,
177 pub link_count: u64,
179 pub metadata: ObjectId,
181 pub payload: FilePayload,
183}
184
185impl FileRecord {
186 pub(crate) fn validate(self) -> Result<(), FileTableError> {
187 if self.link_count == 0 || self.metadata.kind != ObjectKind::Metadata {
188 return Err(FileTableError::InvalidRecord);
189 }
190 let valid = match (self.kind, self.payload) {
191 (FileKind::Regular, FilePayload::Regular { extents, .. }) => {
192 extents.kind == ObjectKind::ExtentPage
193 }
194 (FileKind::Directory, FilePayload::Directory { entries }) => {
195 entries.kind == ObjectKind::TreePage
196 }
197 (FileKind::SymbolicLink, FilePayload::SymbolicLink { target, .. })
198 | (
199 FileKind::ReparsePoint,
200 FilePayload::ReparsePoint {
201 payload: target, ..
202 },
203 ) => target.kind == ObjectKind::Blob,
204 (FileKind::Regular, FilePayload::InlineRegular(_))
205 | (FileKind::CharacterDevice | FileKind::BlockDevice, FilePayload::Device { .. })
206 | (FileKind::Fifo | FileKind::Socket | FileKind::MountBoundary, FilePayload::Empty) => {
207 true
208 }
209 _ => false,
210 };
211 if valid {
212 Ok(())
213 } else {
214 Err(FileTableError::InvalidRecord)
215 }
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub struct FileTableChild {
222 pub first_file_id: FileId,
224 pub page: ObjectId,
226}
227
228#[derive(Clone, Debug, Eq, PartialEq)]
230pub enum FileTablePage {
231 Leaf(Vec<FileRecord>),
233 Internal(Vec<FileTableChild>),
235}
236
237impl FileTablePage {
238 fn validate(&self, maximum_items: u32) -> Result<(), FileTableError> {
239 let count = match self {
240 Self::Leaf(records) => {
241 if records.windows(2).any(|pair| {
242 let [first, second] = pair else {
243 return false;
244 };
245 first.file_id >= second.file_id
246 }) {
247 return Err(FileTableError::NotStrictlyOrdered);
248 }
249 for record in records {
250 record.validate()?;
251 }
252 records.len()
253 }
254 Self::Internal(children) => {
255 if children.is_empty()
256 || children.windows(2).any(|pair| {
257 let [first, second] = pair else {
258 return false;
259 };
260 first.first_file_id >= second.first_file_id
261 })
262 || children
263 .iter()
264 .any(|child| child.page.kind != ObjectKind::FileTablePage)
265 {
266 return Err(FileTableError::InvalidInternalPage);
267 }
268 children.len()
269 }
270 };
271 if u32::try_from(count).unwrap_or(u32::MAX) > maximum_items {
272 return Err(FileTableError::TooManyItems);
273 }
274 Ok(())
275 }
276}
277
278pub fn encode_file_table_page(
284 page: &FileTablePage,
285 maximum_items: u32,
286) -> Result<Vec<u8>, CanonicalDecodeError> {
287 match page {
288 FileTablePage::Leaf(records) => encode_file_table_leaf_records(records, maximum_items),
289 FileTablePage::Internal(children) => encode_file_table_internal_children(
290 children
291 .iter()
292 .map(|child| (child.first_file_id, child.page)),
293 maximum_items,
294 ),
295 }
296}
297
298pub(crate) fn encode_file_table_leaf_records(
299 records: &[FileRecord],
300 maximum_items: u32,
301) -> Result<Vec<u8>, CanonicalDecodeError> {
302 if u32::try_from(records.len()).unwrap_or(u32::MAX) > maximum_items {
303 return Err(invariant(FileTableError::TooManyItems));
304 }
305 if records.windows(2).any(|pair| {
306 let [first, second] = pair else {
307 return false;
308 };
309 first.file_id >= second.file_id
310 }) {
311 return Err(invariant(FileTableError::NotStrictlyOrdered));
312 }
313 let mut encoded_length = DOMAIN
314 .len()
315 .checked_add(2 + 1 + 4)
316 .ok_or(CanonicalDecodeError::LengthOverflow)?;
317 for record in records {
318 record.validate().map_err(invariant)?;
319 encoded_length = encoded_length
320 .checked_add(file_record_encoded_length(*record)?)
321 .ok_or(CanonicalDecodeError::LengthOverflow)?;
322 }
323 let mut encoder = Encoder::with_exact_capacity(DOMAIN, VERSION, encoded_length)?;
324 encoder.u8(1);
325 encoder.u32(count(records.len())?);
326 for record in records {
327 encode_record(&mut encoder, *record);
328 }
329 Ok(encoder.finish())
330}
331
332pub(crate) fn encode_file_table_internal_children<I>(
333 children: I,
334 maximum_items: u32,
335) -> Result<Vec<u8>, CanonicalDecodeError>
336where
337 I: Clone + ExactSizeIterator<Item = (FileId, ObjectId)>,
338{
339 let length = children.len();
340 if length == 0 {
341 return Err(invariant(FileTableError::InvalidInternalPage));
342 }
343 if u32::try_from(length).unwrap_or(u32::MAX) > maximum_items {
344 return Err(invariant(FileTableError::TooManyItems));
345 }
346 let mut prior = None;
347 for (file_id, page) in children.clone() {
348 if prior.is_some_and(|value| value >= file_id) || page.kind != ObjectKind::FileTablePage {
349 return Err(invariant(FileTableError::InvalidInternalPage));
350 }
351 prior = Some(file_id);
352 }
353 let encoded_length = length
354 .checked_mul(16 + 32)
355 .and_then(|items| DOMAIN.len().checked_add(2 + 1 + 4 + items))
356 .ok_or(CanonicalDecodeError::LengthOverflow)?;
357 let mut encoder = Encoder::with_exact_capacity(DOMAIN, VERSION, encoded_length)?;
358 encoder.u8(2);
359 encoder.u32(count(length)?);
360 for (file_id, page) in children {
361 encoder.fixed(&file_id.into_bytes());
362 encoder.fixed(page.digest.as_bytes());
363 }
364 Ok(encoder.finish())
365}
366
367pub(crate) fn file_table_page_decode_shape(
368 bytes: &[u8],
369 limits: DecodeLimits,
370) -> Result<DecodedPageShape, CanonicalDecodeError> {
371 let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
372 let tag = decoder.u8()?;
373 let item_count = decoder.u32()?;
374 if item_count > limits.maximum_page_items {
375 return Err(CanonicalDecodeError::FieldTooLarge {
376 observed: item_count,
377 maximum: limits.maximum_page_items,
378 });
379 }
380 let kind = match tag {
381 1 => {
382 for _ in 0..item_count {
383 let _: [u8; 16] = decoder.fixed()?;
384 decoder.u8()?;
385 decoder.u64()?;
386 let _: [u8; 32] = decoder.fixed()?;
387 match decoder.u8()? {
388 1 | 3 | 6 => {
389 decoder.u64()?;
390 let _: [u8; 32] = decoder.fixed()?;
391 }
392 2 => {
393 let _: [u8; 32] = decoder.fixed()?;
394 }
395 4 => {}
396 5 => {
397 decoder.u32()?;
398 decoder.u32()?;
399 }
400 7 => {
401 let length = decoder.u8()?;
402 if usize::from(length) > MAXIMUM_INLINE_FILE_BYTES {
403 return Err(CanonicalDecodeError::FieldTooLarge {
404 observed: u32::from(length),
405 maximum: u32::try_from(MAXIMUM_INLINE_FILE_BYTES)
406 .map_err(|_| CanonicalDecodeError::LengthOverflow)?,
407 });
408 }
409 decoder.take_exact(usize::from(length))?;
410 }
411 value => {
412 return Err(CanonicalDecodeError::UnknownTag {
413 field: "file_payload",
414 tag: value,
415 });
416 }
417 }
418 }
419 DecodedPageKind::Leaf
420 }
421 2 => {
422 for _ in 0..item_count {
423 let _: [u8; 16] = decoder.fixed()?;
424 let _: [u8; 32] = decoder.fixed()?;
425 }
426 DecodedPageKind::Internal
427 }
428 value => {
429 return Err(CanonicalDecodeError::UnknownTag {
430 field: "file_table_page",
431 tag: value,
432 });
433 }
434 };
435 decoder.finish()?;
436 Ok(DecodedPageShape {
437 kind,
438 items: usize::try_from(item_count).map_err(|_| CanonicalDecodeError::LengthOverflow)?,
439 nested_bytes: 0,
440 })
441}
442
443pub(crate) fn file_record_encoded_length(
444 record: FileRecord,
445) -> Result<usize, CanonicalDecodeError> {
446 const PREFIX: usize = 16 + 1 + 8 + 32 + 1;
447 let payload = match record.payload {
448 FilePayload::InlineRegular(data) => 1_usize
449 .checked_add(data.as_bytes().len())
450 .ok_or(CanonicalDecodeError::LengthOverflow)?,
451 FilePayload::Regular { .. }
452 | FilePayload::SymbolicLink { .. }
453 | FilePayload::ReparsePoint { .. } => 8 + 32,
454 FilePayload::Directory { .. } => 32,
455 FilePayload::Empty => 0,
456 FilePayload::Device { .. } => 8,
457 };
458 PREFIX
459 .checked_add(payload)
460 .ok_or(CanonicalDecodeError::LengthOverflow)
461}
462
463fn encode_record(encoder: &mut Encoder, record: FileRecord) {
464 encoder.fixed(&record.file_id.into_bytes());
465 encoder.u8(record.kind.tag());
466 encoder.u64(record.link_count);
467 encoder.fixed(record.metadata.digest.as_bytes());
468 match record.payload {
469 FilePayload::InlineRegular(data) => {
470 encoder.u8(7);
471 encoder.u8(data.length);
472 encoder.fixed(data.as_bytes());
473 }
474 FilePayload::Regular {
475 logical_bytes,
476 extents,
477 } => {
478 encoder.u8(1);
479 encoder.u64(logical_bytes);
480 encoder.fixed(extents.digest.as_bytes());
481 }
482 FilePayload::Directory { entries } => {
483 encoder.u8(2);
484 encoder.fixed(entries.digest.as_bytes());
485 }
486 FilePayload::SymbolicLink {
487 target_bytes,
488 target,
489 } => {
490 encoder.u8(3);
491 encoder.u64(target_bytes);
492 encoder.fixed(target.digest.as_bytes());
493 }
494 FilePayload::Empty => encoder.u8(4),
495 FilePayload::Device { major, minor } => {
496 encoder.u8(5);
497 encoder.u32(major);
498 encoder.u32(minor);
499 }
500 FilePayload::ReparsePoint {
501 payload_bytes,
502 payload,
503 } => {
504 encoder.u8(6);
505 encoder.u64(payload_bytes);
506 encoder.fixed(payload.digest.as_bytes());
507 }
508 }
509}
510
511pub(crate) fn encode_file_record(record: FileRecord) -> Vec<u8> {
512 let mut encoder = Encoder::new(RECORD_DOMAIN, RECORD_VERSION);
513 encode_record(&mut encoder, record);
514 encoder.finish()
515}
516
517pub(crate) fn decode_file_record(bytes: &[u8]) -> Result<FileRecord, CanonicalDecodeError> {
518 let mut decoder = Decoder::new(
519 bytes,
520 RECORD_DOMAIN,
521 RECORD_VERSION,
522 u64::try_from(bytes.len()).unwrap_or(u64::MAX),
523 )?;
524 let record = decode_record(&mut decoder)?;
525 decoder.finish()?;
526 Ok(record)
527}
528
529pub fn decode_file_table_page(
536 bytes: &[u8],
537 limits: DecodeLimits,
538) -> Result<FileTablePage, CanonicalDecodeError> {
539 let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, limits.maximum_page_object_bytes())?;
540 let tag = decoder.u8()?;
541 let item_count = decoder.u32()?;
542 if item_count > limits.maximum_page_items {
543 return Err(CanonicalDecodeError::FieldTooLarge {
544 observed: item_count,
545 maximum: limits.maximum_page_items,
546 });
547 }
548 let mut records = Vec::new();
549 let mut children = Vec::new();
550 match tag {
551 1 => {
552 records
553 .try_reserve_exact(capacity(item_count)?)
554 .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
555 for _ in 0..item_count {
556 records.push(decode_record(&mut decoder)?);
557 }
558 }
559 2 => {
560 children
561 .try_reserve_exact(capacity(item_count)?)
562 .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
563 for _ in 0..item_count {
564 children.push(FileTableChild {
565 first_file_id: FileId::from_bytes(decoder.fixed()?),
566 page: digest_object(ObjectKind::FileTablePage, decoder.fixed()?),
567 });
568 }
569 }
570 value => {
571 return Err(CanonicalDecodeError::UnknownTag {
572 field: "file_table_page",
573 tag: value,
574 });
575 }
576 }
577 decoder.finish()?;
578 let page = if tag == 1 {
579 FileTablePage::Leaf(records)
580 } else {
581 FileTablePage::Internal(children)
582 };
583 page.validate(limits.maximum_page_items)
584 .map_err(invariant)?;
585 Ok(page)
586}
587
588fn decode_record(decoder: &mut Decoder<'_>) -> Result<FileRecord, CanonicalDecodeError> {
589 let file_id = FileId::from_bytes(decoder.fixed()?);
590 let kind = FileKind::from_tag(decoder.u8()?).map_err(invariant_tree)?;
591 let link_count = decoder.u64()?;
592 let metadata = digest_object(ObjectKind::Metadata, decoder.fixed()?);
593 let payload = match decoder.u8()? {
594 1 => FilePayload::Regular {
595 logical_bytes: decoder.u64()?,
596 extents: digest_object(ObjectKind::ExtentPage, decoder.fixed()?),
597 },
598 2 => FilePayload::Directory {
599 entries: digest_object(ObjectKind::TreePage, decoder.fixed()?),
600 },
601 3 => FilePayload::SymbolicLink {
602 target_bytes: decoder.u64()?,
603 target: digest_object(ObjectKind::Blob, decoder.fixed()?),
604 },
605 4 => FilePayload::Empty,
606 5 => FilePayload::Device {
607 major: decoder.u32()?,
608 minor: decoder.u32()?,
609 },
610 6 => FilePayload::ReparsePoint {
611 payload_bytes: decoder.u64()?,
612 payload: digest_object(ObjectKind::Blob, decoder.fixed()?),
613 },
614 7 => {
615 let length = decoder.u8()?;
616 if usize::from(length) > MAXIMUM_INLINE_FILE_BYTES {
617 return Err(CanonicalDecodeError::FieldTooLarge {
618 observed: u32::from(length),
619 maximum: u32::try_from(MAXIMUM_INLINE_FILE_BYTES)
620 .map_err(|_| CanonicalDecodeError::LengthOverflow)?,
621 });
622 }
623 let bytes = decoder.take_exact(usize::from(length))?;
624 FilePayload::InlineRegular(InlineFileData::new(bytes).map_err(invariant_inline)?)
625 }
626 tag => {
627 return Err(CanonicalDecodeError::UnknownTag {
628 field: "file_payload",
629 tag,
630 });
631 }
632 };
633 let record = FileRecord {
634 file_id,
635 kind,
636 link_count,
637 metadata,
638 payload,
639 };
640 record.validate().map_err(invariant)?;
641 Ok(record)
642}
643
644pub fn file_table_page_id(
650 page: &FileTablePage,
651 maximum_items: u32,
652) -> Result<ObjectId, CanonicalDecodeError> {
653 let bytes = encode_file_table_page(page, maximum_items)?;
654 Ok(ObjectId {
655 kind: ObjectKind::FileTablePage,
656 digest: object_digest(ObjectKind::FileTablePage, &bytes),
657 })
658}
659
660#[derive(Clone, Debug, Eq, PartialEq)]
662pub struct FileRecordLookup {
663 pub record: Option<FileRecord>,
665 pub work: WorkCounters,
667}
668
669#[derive(Clone, Debug, Eq, PartialEq)]
671pub struct FileRecordBatchLookup {
672 pub records: Vec<Option<FileRecord>>,
674 pub work: WorkCounters,
676}
677
678pub fn lookup_file_records<S: crate::ImmediateObjectStore>(
685 store: &S,
686 root: ObjectId,
687 file_ids: &[FileId],
688 maximum_queries: u32,
689 limits: DecodeLimits,
690 budget: WorkBudget,
691) -> Result<FileRecordBatchLookup, FileRecordReadFailure> {
692 persistent_batch::lookup::<S, FileTableFormat>(
693 store,
694 root,
695 file_ids,
696 maximum_queries,
697 limits,
698 budget,
699 )
700 .map(to_record_batch)
701 .map_err(map_batch_failure)
702}
703
704pub async fn lookup_file_records_async<S: AsyncObjectStore>(
710 store: &S,
711 root: ObjectId,
712 file_ids: &[FileId],
713 maximum_queries: u32,
714 limits: DecodeLimits,
715 budget: WorkBudget,
716 cancellation: &CancellationToken,
717) -> Result<FileRecordBatchLookup, FileRecordReadFailure> {
718 persistent_batch::lookup_async::<S, FileTableFormat>(
719 store,
720 root,
721 file_ids,
722 maximum_queries,
723 limits,
724 budget,
725 cancellation,
726 )
727 .await
728 .map(to_record_batch)
729 .map_err(map_batch_failure)
730}
731
732fn to_record_batch(receipt: persistent_batch::Receipt<FileRecord>) -> FileRecordBatchLookup {
733 FileRecordBatchLookup {
734 records: receipt.values,
735 work: receipt.work,
736 }
737}
738
739fn map_batch_failure(failure: persistent_batch::Failure) -> FileRecordReadFailure {
740 OperationFailure::new(map_batch_error(failure.error), *failure.work)
741}
742
743fn map_batch_error(error: persistent_batch::Error) -> FileRecordReadError {
744 match error {
745 persistent_batch::Error::Cancelled => FileRecordReadError::Cancelled,
746 persistent_batch::Error::Empty => FileRecordReadError::EmptyBatch,
747 persistent_batch::Error::TooManyQueries => FileRecordReadError::TooManyQueries,
748 persistent_batch::Error::WrongRootKind => FileRecordReadError::WrongRootKind,
749 persistent_batch::Error::InvalidLimits => FileRecordReadError::InvalidHeightLimit,
750 persistent_batch::Error::HeightExceeded => FileRecordReadError::HeightExceeded,
751 persistent_batch::Error::CycleOrAlias => FileRecordReadError::Cycle,
752 persistent_batch::Error::ChildBoundsMismatch => FileRecordReadError::ChildBoundsMismatch,
753 persistent_batch::Error::InvalidRouting => FileRecordReadError::InvalidRouting,
754 persistent_batch::Error::AllocationFailed => FileRecordReadError::AllocationFailed,
755 persistent_batch::Error::Storage(error) => error.into(),
756 persistent_batch::Error::Decode(error) => error.into(),
757 persistent_batch::Error::Work(error) => error.into(),
758 }
759}
760
761pub fn lookup_file_record<S: ObjectStore>(
768 store: &S,
769 root: ObjectId,
770 file_id: FileId,
771 limits: DecodeLimits,
772 budget: WorkBudget,
773) -> Result<FileRecordLookup, FileRecordReadFailure> {
774 let mut machine = FileLookupMachine::new(root, file_id, limits, budget)?;
775 frontier::drive_sync(store, &mut machine)
776}
777
778pub async fn lookup_file_record_async<S: AsyncObjectStore>(
786 store: &S,
787 root: ObjectId,
788 file_id: FileId,
789 limits: DecodeLimits,
790 budget: WorkBudget,
791 cancellation: &CancellationToken,
792) -> Result<FileRecordLookup, FileRecordReadFailure> {
793 let mut machine = FileLookupMachine::new(root, file_id, limits, budget)?;
794 frontier::drive_async(store, &mut machine, cancellation).await
795}
796
797struct FileLookupMachine {
798 file_id: FileId,
799 limits: DecodeLimits,
800 budget: WorkBudget,
801 page: ObjectId,
802 lower: Option<FileId>,
803 upper: Option<FileId>,
804 visited: HashSet<ObjectId>,
805 work: WorkCounters,
806}
807
808impl FileLookupMachine {
809 fn new(
810 root: ObjectId,
811 file_id: FileId,
812 limits: DecodeLimits,
813 budget: WorkBudget,
814 ) -> Result<Self, FileRecordReadFailure> {
815 if root.kind != ObjectKind::FileTablePage {
816 return Err(file_read_failed(
817 FileRecordReadError::WrongRootKind,
818 WorkCounters::default(),
819 ));
820 }
821 if !limits.page_limits_valid(1) {
822 return Err(file_read_failed(
823 FileRecordReadError::InvalidHeightLimit,
824 WorkCounters::default(),
825 ));
826 }
827 Ok(Self {
828 file_id,
829 limits,
830 budget,
831 page: root,
832 lower: None,
833 upper: None,
834 visited: HashSet::new(),
835 work: WorkCounters::default(),
836 })
837 }
838
839 fn prepare_read(&mut self) -> Result<frontier::ReadRequest, FileRecordReadFailure> {
840 if self.visited.len() >= usize::from(self.limits.maximum_page_height) {
841 return Err(file_read_failed(
842 FileRecordReadError::HeightExceeded,
843 self.work,
844 ));
845 }
846 if !self.visited.insert(self.page) {
847 return Err(file_read_failed(FileRecordReadError::Cycle, self.work));
848 }
849 let prospective = self
850 .work
851 .checked_add(WorkCounters {
852 page_reads: 1,
853 ..WorkCounters::default()
854 })
855 .map_err(|error| file_read_failed(error.into(), self.work))?;
856 let remaining = prospective
857 .remaining(self.budget)
858 .map_err(|error| file_read_failed(error.into(), self.work))?;
859 Ok(frontier::ReadRequest {
860 page: self.page,
861 maximum_bytes: self.limits.maximum_page_object_bytes(),
862 remaining,
863 prospective,
864 })
865 }
866
867 fn accept(
868 &mut self,
869 prospective: WorkCounters,
870 receipt: &ObjectReceipt<ObjectRead>,
871 ) -> Result<Option<FileRecordLookup>, FileRecordReadFailure> {
872 self.work = prospective
873 .checked_add(receipt.work)
874 .map_err(|error| file_read_failed(error.into(), prospective))?;
875 self.work
876 .verify(self.budget)
877 .map_err(|error| file_read_failed(error.into(), self.work))?;
878 match decode_file_table_page(&receipt.value, self.limits)
879 .map_err(|error| file_read_failed(error.into(), self.work))?
880 {
881 FileTablePage::Leaf(records) => {
882 validate_record_bounds(&records, self.lower, self.upper)
883 .map_err(|error| file_read_failed(error, self.work))?;
884 let record = records
885 .binary_search_by_key(&self.file_id, |record| record.file_id)
886 .ok()
887 .and_then(|index| records.get(index).copied());
888 Ok(Some(FileRecordLookup {
889 record,
890 work: self.work,
891 }))
892 }
893 FileTablePage::Internal(children) => {
894 validate_child_bounds(&children, self.lower, self.upper)
895 .map_err(|error| file_read_failed(error, self.work))?;
896 let partition =
897 children.partition_point(|child| child.first_file_id <= self.file_id);
898 let selected = partition.saturating_sub(1);
899 let child = children.get(selected).copied().ok_or_else(|| {
900 file_read_failed(FileRecordReadError::InvalidRouting, self.work)
901 })?;
902 self.lower = Some(child.first_file_id);
903 self.upper = children
904 .get(selected + 1)
905 .map(|next| next.first_file_id)
906 .or(self.upper);
907 self.page = child.page;
908 Ok(None)
909 }
910 }
911 }
912}
913
914impl frontier::Machine for FileLookupMachine {
915 type Output = FileRecordLookup;
916 type Failure = FileRecordReadFailure;
917
918 fn complete(&mut self) -> Result<Option<Self::Output>, Self::Failure> {
919 Ok(None)
920 }
921
922 fn prepare_read(&mut self) -> Result<frontier::ReadRequest, Self::Failure> {
923 FileLookupMachine::prepare_read(self)
924 }
925
926 fn accept(
927 &mut self,
928 prospective: WorkCounters,
929 receipt: &ObjectReceipt<ObjectRead>,
930 ) -> Result<Option<Self::Output>, Self::Failure> {
931 FileLookupMachine::accept(self, prospective, receipt)
932 }
933
934 fn storage_failure(
935 &self,
936 prospective: WorkCounters,
937 failure: crate::storage::ObjectFailure,
938 ) -> Self::Failure {
939 match prospective.checked_add(*failure.work) {
940 Ok(spent) => file_read_failed(failure.error.into(), spent),
941 Err(error) => file_read_failed(error.into(), prospective),
942 }
943 }
944
945 fn cancelled(&self) -> Self::Failure {
946 file_read_failed(FileRecordReadError::Cancelled, self.work)
947 }
948}
949
950fn validate_record_bounds(
951 records: &[FileRecord],
952 lower: Option<FileId>,
953 upper: Option<FileId>,
954) -> Result<(), FileRecordReadError> {
955 if lower.is_some() && records.first().map(|record| record.file_id) != lower {
956 return Err(FileRecordReadError::ChildBoundsMismatch);
957 }
958 if let Some(upper) = upper
959 && records.last().is_some_and(|record| record.file_id >= upper)
960 {
961 return Err(FileRecordReadError::ChildBoundsMismatch);
962 }
963 Ok(())
964}
965
966fn validate_child_bounds(
967 children: &[FileTableChild],
968 lower: Option<FileId>,
969 upper: Option<FileId>,
970) -> Result<(), FileRecordReadError> {
971 if lower.is_some() && children.first().map(|child| child.first_file_id) != lower {
972 return Err(FileRecordReadError::ChildBoundsMismatch);
973 }
974 if let Some(upper) = upper
975 && children
976 .last()
977 .is_some_and(|child| child.first_file_id >= upper)
978 {
979 return Err(FileRecordReadError::ChildBoundsMismatch);
980 }
981 Ok(())
982}
983
984pub type FileRecordReadFailure = OperationFailure<FileRecordReadError>;
986
987fn file_read_failed(error: FileRecordReadError, work: WorkCounters) -> FileRecordReadFailure {
988 OperationFailure::new(error, work)
989}
990
991#[derive(Debug, Error)]
993pub enum FileRecordReadError {
994 #[error("file-record lookup was cancelled")]
996 Cancelled,
997 #[error("file-record lookup batch is empty")]
999 EmptyBatch,
1000 #[error("file-record lookup batch exceeds its admitted query bound")]
1002 TooManyQueries,
1003 #[error("file-record lookup root is not a file-table page")]
1005 WrongRootKind,
1006 #[error("file-table height limit must be non-zero")]
1008 InvalidHeightLimit,
1009 #[error("file-table graph contains a cycle")]
1011 Cycle,
1012 #[error("file-table height exceeds its admitted bound")]
1014 HeightExceeded,
1015 #[error("file-table routing invariant failed")]
1017 InvalidRouting,
1018 #[error("file-table child bounds do not match its page")]
1020 ChildBoundsMismatch,
1021 #[error("file-record lookup allocation failed")]
1023 AllocationFailed,
1024 #[error(transparent)]
1026 Storage(#[from] ObjectStoreError),
1027 #[error(transparent)]
1029 Decode(#[from] CanonicalDecodeError),
1030 #[error(transparent)]
1032 Work(#[from] WorkError),
1033}
1034
1035#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
1037pub enum FileTableError {
1038 #[error("file records are not strictly ordered")]
1040 NotStrictlyOrdered,
1041 #[error("file record is inconsistent")]
1043 InvalidRecord,
1044 #[error("file-table internal page is invalid")]
1046 InvalidInternalPage,
1047 #[error("file-table page exceeds its item bound")]
1049 TooManyItems,
1050}
1051
1052fn count(value: usize) -> Result<u32, CanonicalDecodeError> {
1053 u32::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
1054}
1055
1056fn capacity(value: u32) -> Result<usize, CanonicalDecodeError> {
1057 usize::try_from(value).map_err(|_| CanonicalDecodeError::LengthOverflow)
1058}
1059
1060fn invariant(error: FileTableError) -> CanonicalDecodeError {
1061 CanonicalDecodeError::Invariant(error.to_string())
1062}
1063
1064fn invariant_tree(error: super::TreePageError) -> CanonicalDecodeError {
1065 CanonicalDecodeError::Invariant(error.to_string())
1066}
1067
1068fn invariant_inline(error: InlineFileDataError) -> CanonicalDecodeError {
1069 CanonicalDecodeError::Invariant(error.to_string())
1070}
1071
1072#[cfg(test)]
1073#[path = "tests/file_table.rs"]
1074mod tests;