1use crate::fsck::errors::{FsckError, FsckFatal, FsckWarning};
6use crate::fsck::{FragmentationStats, Fsck, FsckResult};
7use crate::lsm_tree::Query;
8use crate::lsm_tree::types::{Item, ItemRef, LayerIterator};
9use crate::object_handle::INVALID_OBJECT_ID;
10use crate::object_store::allocator::{self, AllocatorKey, AllocatorValue};
11use crate::object_store::directory::decrypt_filename;
12use crate::object_store::graveyard::Graveyard;
13use crate::object_store::object_record::EncryptedCasefoldChild;
14use crate::object_store::{
15 AttributeKey, ChildValue, DEFAULT_DATA_ATTRIBUTE_ID, EXTENDED_ATTRIBUTE_RANGE_END,
16 EXTENDED_ATTRIBUTE_RANGE_START, ExtendedAttributeValue, ExtentKey, ExtentMode, ExtentValue,
17 FSCRYPT_KEY_ID, FSVERITY_MERKLE_ATTRIBUTE_ID, FsverityMetadata, ObjectAttributes,
18 ObjectDescriptor, ObjectKey, ObjectKeyData, ObjectKind, ObjectStore, ObjectValue,
19 ProjectProperty, RootDigest, VOLUME_DATA_KEY_ID,
20};
21use crate::range::RangeExt;
22use crate::round::round_up;
23use anyhow::{Error, bail};
24use fxfs_crypto::{Crypt, WrappedKey, WrappingKeyId, key_to_cipher};
25use rustc_hash::FxHashSet as HashSet;
26use std::cell::UnsafeCell;
27use std::collections::btree_map::BTreeMap;
28use std::ops::Range;
29use std::sync::Arc;
30
31#[derive(Debug)]
33struct ScannedAttribute {
34 attribute_id: u64,
37 size: u64,
39 has_overwrite_extents_flag: Option<bool>,
43 observed_overwrite_extents: bool,
45}
46
47#[derive(Debug)]
49struct ScannedAttributes {
50 attributes: Vec<ScannedAttribute>,
51 tombstoned_attributes: Vec<u64>,
53 stored_allocated_size: u64,
55 observed_allocated_size: u64,
57 in_graveyard: bool,
59 extended_attributes: Vec<u64>,
61}
62
63#[derive(Clone, Debug)]
65enum VerifiedType {
66 None,
67 Internal(usize),
69 F2fs(u64),
71}
72
73#[derive(Debug)]
74struct ScannedFile {
75 parents: Vec<u64>,
79 stored_refs: u64,
81 attributes: ScannedAttributes,
83 verified: VerifiedType,
85}
86
87#[derive(Debug)]
88struct ScannedDir {
89 stored_sub_dirs: u64,
91 observed_sub_dirs: u64,
93 parent: Option<u64>,
96 visited: UnsafeCell<bool>,
98 wrapping_key_id: Option<WrappingKeyId>,
100 attributes: ScannedAttributes,
102 casefold: bool,
104 fscrypt_key: Option<WrappedKey>,
106}
107
108unsafe impl Sync for ScannedDir {}
109
110#[derive(Debug)]
111struct ScannedSymlink {
112 parents: Vec<u64>,
114 stored_refs: u64,
116 attributes: ScannedAttributes,
118 encrypted: bool,
119}
120
121#[derive(Debug)]
122enum ScannedObject {
123 Directory(ScannedDir),
124 File(ScannedFile),
125 Graveyard,
126 Symlink(ScannedSymlink),
127 Tombstone,
129}
130
131struct ScannedStore<'a> {
132 fsck: &'a Fsck<'a>,
133 crypt: Option<Arc<dyn Crypt>>,
134 objects: BTreeMap<u64, ScannedObject>,
135 root_objects: Vec<u64>,
136 store_id: u64,
137 is_root_store: bool,
138 is_encrypted: bool,
139 current_object: Option<CurrentObject>,
140 root_dir_id: u64,
141 stored_project_usages: BTreeMap<u64, (i64, i64)>,
142 total_project_usages: BTreeMap<u64, (i64, i64)>,
143 used_project_ids: BTreeMap<u64, u64>,
145}
146
147struct CurrentObject {
148 object_id: u64,
149 key_ids: HashSet<u64>,
150 lazy_keys: bool,
151}
152
153impl<'a> ScannedStore<'a> {
154 fn new(
155 fsck: &'a Fsck<'a>,
156 crypt: Option<Arc<dyn Crypt>>,
157 root_objects: impl AsRef<[u64]>,
158 store_id: u64,
159 is_root_store: bool,
160 is_encrypted: bool,
161 root_dir_id: u64,
162 ) -> Self {
163 Self {
164 fsck,
165 crypt,
166 objects: BTreeMap::new(),
167 root_objects: root_objects.as_ref().into(),
168 store_id,
169 is_root_store,
170 is_encrypted,
171 current_object: None,
172 root_dir_id,
173 stored_project_usages: BTreeMap::new(),
174 total_project_usages: BTreeMap::new(),
175 used_project_ids: BTreeMap::new(),
176 }
177 }
178
179 fn process(&mut self, key: &ObjectKey, value: &ObjectValue) -> Result<(), Error> {
181 match key.data {
182 ObjectKeyData::Object => {
183 match value {
184 ObjectValue::None => {
185 if self.objects.insert(key.object_id, ScannedObject::Tombstone).is_some() {
186 self.fsck.error(FsckError::TombstonedObjectHasRecords(
187 self.store_id,
188 key.object_id,
189 ))?;
190 }
191 }
192 ObjectValue::Some => {
193 self.fsck.error(FsckError::UnexpectedRecordInObjectStore(
194 self.store_id,
195 key.into(),
196 value.into(),
197 ))?;
198 }
199 ObjectValue::Object {
200 kind: ObjectKind::File { refs },
201 attributes: ObjectAttributes { project_id, allocated_size, .. },
202 } => {
203 if *project_id > 0 {
204 self.used_project_ids.insert(*project_id, key.object_id);
205 let entry = self.total_project_usages.entry(*project_id).or_default();
206 entry.0 += i64::try_from(*allocated_size).unwrap();
207 entry.1 += 1;
208 }
209 self.current_object = Some(CurrentObject {
210 object_id: key.object_id,
211 key_ids: HashSet::default(),
212 lazy_keys: false,
213 });
214 let parents = if self.root_objects.contains(&key.object_id) {
215 vec![INVALID_OBJECT_ID]
216 } else {
217 vec![]
218 };
219 self.objects.insert(
220 key.object_id,
221 ScannedObject::File(ScannedFile {
222 parents,
223 stored_refs: *refs,
224 attributes: ScannedAttributes {
225 attributes: Vec::new(),
226 tombstoned_attributes: Vec::new(),
227 stored_allocated_size: *allocated_size,
228 observed_allocated_size: 0,
229 in_graveyard: false,
230 extended_attributes: Vec::new(),
231 },
232 verified: VerifiedType::None,
233 }),
234 );
235 }
236 ObjectValue::Object {
237 kind: ObjectKind::Directory { sub_dirs, casefold, wrapping_key_id },
239 attributes: ObjectAttributes { project_id, allocated_size, .. },
240 } => {
241 if *project_id > 0 {
242 self.used_project_ids.insert(*project_id, key.object_id);
243 let entry = self.total_project_usages.entry(*project_id).or_default();
244 entry.1 += 1;
246 }
247 let parent = if self.root_objects.contains(&key.object_id) {
248 Some(INVALID_OBJECT_ID)
249 } else {
250 None
251 };
252 self.current_object = Some(CurrentObject {
253 object_id: key.object_id,
254 key_ids: HashSet::default(),
255 lazy_keys: true,
256 });
257 self.objects.insert(
260 key.object_id,
261 ScannedObject::Directory(ScannedDir {
262 stored_sub_dirs: *sub_dirs,
263 observed_sub_dirs: 0,
264 parent,
265 visited: UnsafeCell::new(false),
266 wrapping_key_id: *wrapping_key_id,
267 attributes: ScannedAttributes {
268 attributes: Vec::new(),
269 tombstoned_attributes: Vec::new(),
270 stored_allocated_size: *allocated_size,
271 observed_allocated_size: 0,
272 in_graveyard: false,
273 extended_attributes: Vec::new(),
274 },
275 casefold: *casefold,
276 fscrypt_key: None,
277 }),
278 );
279 }
280 ObjectValue::Object { kind: ObjectKind::Graveyard, attributes } => {
281 self.objects.insert(key.object_id, ScannedObject::Graveyard);
282 if attributes.project_id != 0 {
283 self.fsck.error(FsckError::ProjectOnGraveyard(
284 self.store_id,
285 attributes.project_id,
286 key.object_id,
287 ))?;
288 }
289 }
290 ObjectValue::Object {
291 kind: ObjectKind::Symlink { refs, .. },
292 attributes: ObjectAttributes { project_id, allocated_size, .. },
293 } => {
294 if *project_id > 0 {
295 self.used_project_ids.insert(*project_id, key.object_id);
296 let entry = self.total_project_usages.entry(*project_id).or_default();
297 entry.1 += 1;
299 }
300 self.current_object = Some(CurrentObject {
301 object_id: key.object_id,
302 key_ids: HashSet::default(),
303 lazy_keys: true,
304 });
305 self.objects.insert(
306 key.object_id,
307 ScannedObject::Symlink(ScannedSymlink {
308 parents: vec![],
309 stored_refs: *refs,
310 encrypted: false,
311 attributes: ScannedAttributes {
312 attributes: Vec::new(),
313 tombstoned_attributes: Vec::new(),
314 stored_allocated_size: *allocated_size,
315 observed_allocated_size: 0,
316 in_graveyard: false,
317 extended_attributes: Vec::new(),
318 },
319 }),
320 );
321 }
322 ObjectValue::Object {
323 kind: ObjectKind::EncryptedSymlink { refs, .. },
324 attributes: ObjectAttributes { project_id, allocated_size, .. },
325 } => {
326 if *project_id > 0 {
327 self.used_project_ids.insert(*project_id, key.object_id);
328 let entry = self.total_project_usages.entry(*project_id).or_default();
329 entry.1 += 1;
331 }
332 self.current_object = Some(CurrentObject {
333 object_id: key.object_id,
334 key_ids: HashSet::default(),
335 lazy_keys: true,
336 });
337 self.objects.insert(
338 key.object_id,
339 ScannedObject::Symlink(ScannedSymlink {
340 parents: vec![],
341 stored_refs: *refs,
342 encrypted: true,
343 attributes: ScannedAttributes {
344 attributes: Vec::new(),
345 tombstoned_attributes: Vec::new(),
346 stored_allocated_size: *allocated_size,
347 observed_allocated_size: 0,
348 in_graveyard: false,
349 extended_attributes: Vec::new(),
350 },
351 }),
352 );
353 }
354 _ => {
355 self.fsck.error(FsckError::MalformedObjectRecord(
356 self.store_id,
357 key.into(),
358 value.into(),
359 ))?;
360 }
361 }
362 }
363 ObjectKeyData::Keys => {
364 if let ObjectValue::Keys(keys) = value {
365 if let Some(current_file) = &mut self.current_object {
366 assert!(current_file.key_ids.is_empty());
369 for (key_id, encryption_key) in keys.iter() {
370 if !current_file.key_ids.insert(*key_id) {
371 self.fsck.error(FsckError::DuplicateKey(
372 self.store_id,
373 key.object_id,
374 *key_id,
375 ))?;
376 }
377 if *key_id == FSCRYPT_KEY_ID {
378 if let Some(ScannedObject::Directory(dir)) =
379 self.objects.get_mut(¤t_file.object_id)
380 {
381 dir.fscrypt_key = Some(encryption_key.clone().into());
382 }
383 }
384 }
385 } else {
386 self.fsck
387 .warning(FsckWarning::OrphanedKeys(self.store_id, key.object_id))?;
388 }
389 } else {
390 self.fsck.error(FsckError::MalformedObjectRecord(
391 self.store_id,
392 key.into(),
393 value.into(),
394 ))?;
395 }
396 }
397 ObjectKeyData::Attribute(attribute_id, AttributeKey::Attribute) => {
398 match value {
399 ObjectValue::Attribute { size, has_overwrite_extents } => {
400 match self.objects.get_mut(&key.object_id) {
401 Some(
402 ScannedObject::File(ScannedFile { attributes, .. })
403 | ScannedObject::Directory(ScannedDir { attributes, .. })
404 | ScannedObject::Symlink(ScannedSymlink { attributes, .. }),
405 ) => {
406 attributes.attributes.push(ScannedAttribute {
407 attribute_id,
408 size: *size,
409 has_overwrite_extents_flag: Some(*has_overwrite_extents),
410 observed_overwrite_extents: false,
411 });
412 }
413 Some(ScannedObject::Graveyard) => { }
414 Some(ScannedObject::Tombstone) => {
415 self.fsck.error(FsckError::TombstonedObjectHasRecords(
416 self.store_id,
417 key.object_id,
418 ))?;
419 }
420 None => {
421 self.fsck.warning(FsckWarning::OrphanedAttribute(
426 self.store_id,
427 key.object_id,
428 attribute_id,
429 ))?;
430 }
431 }
432 }
433 ObjectValue::VerifiedAttribute { size, fsverity_metadata } => {
434 match self.objects.get_mut(&key.object_id) {
435 Some(ScannedObject::File(ScannedFile {
436 attributes,
437 verified: is_verified,
438 ..
439 })) => {
440 attributes.attributes.push(ScannedAttribute {
441 attribute_id,
442 size: *size,
443 has_overwrite_extents_flag: None,
444 observed_overwrite_extents: false,
445 });
446 *is_verified = match &fsverity_metadata {
447 FsverityMetadata::Internal(
448 RootDigest::Sha256(root_hash),
449 _,
450 ) => VerifiedType::Internal(root_hash.len()),
451 FsverityMetadata::Internal(
452 RootDigest::Sha512(root_hash),
453 _,
454 ) => VerifiedType::Internal(root_hash.len()),
455 FsverityMetadata::F2fs(range) => VerifiedType::F2fs(range.end),
456 };
457 }
458 Some(ScannedObject::Directory(..) | ScannedObject::Symlink(..)) => {
459 self.fsck.error(FsckError::NonFileMarkedAsVerified(
460 self.store_id,
461 key.object_id,
462 ))?;
463 }
464 Some(ScannedObject::Graveyard) => { }
465 Some(ScannedObject::Tombstone) => {
466 self.fsck.error(FsckError::TombstonedObjectHasRecords(
467 self.store_id,
468 key.object_id,
469 ))?;
470 }
471 None => {
472 self.fsck.warning(FsckWarning::OrphanedAttribute(
473 self.store_id,
474 key.object_id,
475 attribute_id,
476 ))?;
477 }
478 }
479 }
480 ObjectValue::None => (),
482 _ => {
483 self.fsck.error(FsckError::MalformedObjectRecord(
484 self.store_id,
485 key.into(),
486 value.into(),
487 ))?;
488 }
489 }
490 }
491 ObjectKeyData::Attribute(_, AttributeKey::Extent(_)) => {
493 match value {
494 ObjectValue::Extent(ExtentValue::Some { key_id, .. }) => {
496 if let Some(current_file) = &self.current_object {
497 if !self.is_encrypted && *key_id == 0 && current_file.key_ids.is_empty()
498 {
499 } else if !current_file.key_ids.contains(key_id) {
501 self.fsck.error(FsckError::MissingKey(
502 self.store_id,
503 key.object_id,
504 *key_id,
505 ))?;
506 }
507 } else {
508 }
510 }
511 ObjectValue::Extent(ExtentValue::None) => {}
513 _ => {
514 self.fsck.error(FsckError::MalformedObjectRecord(
515 self.store_id,
516 key.into(),
517 value.into(),
518 ))?;
519 }
520 }
521 }
522 ObjectKeyData::Child { .. }
524 | ObjectKeyData::CasefoldChild { .. }
525 | ObjectKeyData::EncryptedChild(_)
526 | ObjectKeyData::EncryptedCasefoldChild(_) => match value {
527 ObjectValue::None => {}
528 ObjectValue::Child(ChildValue { object_id: child_id, object_descriptor }) => {
529 if *child_id == INVALID_OBJECT_ID {
530 self.fsck.warning(FsckWarning::InvalidObjectIdInStore(
531 self.store_id,
532 key.into(),
533 value.into(),
534 ))?;
535 }
536 if self.root_objects.contains(child_id) {
537 self.fsck.error(FsckError::RootObjectHasParent(
538 self.store_id,
539 *child_id,
540 key.object_id,
541 ))?;
542 }
543 if object_descriptor == &ObjectDescriptor::Volume && !self.is_root_store {
544 self.fsck.error(FsckError::VolumeInChildStore(self.store_id, *child_id))?;
545 }
546 }
547 _ => {
548 self.fsck.error(FsckError::MalformedObjectRecord(
549 self.store_id,
550 key.into(),
551 value.into(),
552 ))?;
553 }
554 },
555 ObjectKeyData::Project { project_id, property: ProjectProperty::Limit } => {
556 if self.root_dir_id != key.object_id {
558 self.fsck.error(FsckError::NonRootProjectIdMetadata(
559 self.store_id,
560 key.object_id,
561 project_id,
562 ))?;
563 }
564 match value {
565 ObjectValue::None | ObjectValue::BytesAndNodes { .. } => {}
566 _ => {
567 self.fsck.error(FsckError::MalformedObjectRecord(
568 self.store_id,
569 key.into(),
570 value.into(),
571 ))?;
572 }
573 }
574 }
575 ObjectKeyData::Project { project_id, property: ProjectProperty::Usage } => {
576 if self.root_dir_id != key.object_id {
578 self.fsck.error(FsckError::NonRootProjectIdMetadata(
579 self.store_id,
580 key.object_id,
581 project_id,
582 ))?;
583 }
584 match value {
585 ObjectValue::None => {
586 self.stored_project_usages.remove(&project_id);
587 }
588 ObjectValue::BytesAndNodes { bytes, nodes } => {
589 self.stored_project_usages.insert(project_id, (*bytes, *nodes));
590 }
591 _ => {
592 self.fsck.error(FsckError::MalformedObjectRecord(
593 self.store_id,
594 key.into(),
595 value.into(),
596 ))?;
597 }
598 }
599 }
600 ObjectKeyData::ExtendedAttribute { .. } => match value {
601 ObjectValue::None => {}
602 ObjectValue::ExtendedAttribute(ExtendedAttributeValue::Inline(_)) => {
603 if self.objects.get(&key.object_id).is_none() {
604 self.fsck.warning(FsckWarning::OrphanedExtendedAttributeRecord(
605 self.store_id,
606 key.object_id,
607 ))?;
608 }
609 }
610 ObjectValue::ExtendedAttribute(ExtendedAttributeValue::AttributeId(id)) => {
611 match self.objects.get_mut(&key.object_id) {
612 Some(
613 ScannedObject::File(ScannedFile { attributes, .. })
614 | ScannedObject::Directory(ScannedDir { attributes, .. })
615 | ScannedObject::Symlink(ScannedSymlink { attributes, .. }),
616 ) => {
617 attributes.extended_attributes.push(*id);
618 }
619 Some(ScannedObject::Graveyard) => { }
620 Some(ScannedObject::Tombstone) => {
621 self.fsck.error(FsckError::TombstonedObjectHasRecords(
622 self.store_id,
623 key.object_id,
624 ))?;
625 }
626 None => {
627 self.fsck.warning(FsckWarning::OrphanedExtendedAttributeRecord(
628 self.store_id,
629 key.object_id,
630 ))?;
631 }
632 }
633 }
634 _ => {
635 self.fsck.error(FsckError::MalformedObjectRecord(
636 self.store_id,
637 key.into(),
638 value.into(),
639 ))?;
640 }
641 },
642 ObjectKeyData::GraveyardEntry { .. } => {}
643 ObjectKeyData::GraveyardAttributeEntry { .. } => {}
644 }
645 Ok(())
646 }
647
648 async fn process_child(
651 &mut self,
652 parent_id: u64,
653 child_id: u64,
654 object_descriptor: &ObjectDescriptor,
655 object_key_data: &ObjectKeyData,
656 ) -> Result<(), Error> {
657 let mut child_wrapping_key_id = None;
658 if let Some(ScannedObject::Directory(dir)) = self.objects.get(&parent_id) {
659 match object_key_data {
660 ObjectKeyData::Child { .. } => {
661 if dir.casefold {
662 self.fsck.error(FsckError::CasefoldInconsistency(
663 self.store_id,
664 parent_id,
665 child_id,
666 ))?;
667 }
668 }
669 ObjectKeyData::CasefoldChild { .. } => {
670 if !dir.casefold {
671 self.fsck.error(FsckError::CasefoldInconsistency(
672 self.store_id,
673 parent_id,
674 child_id,
675 ))?;
676 }
677 }
678 ObjectKeyData::EncryptedCasefoldChild(EncryptedCasefoldChild {
679 hash_code,
680 name,
681 }) => {
682 if dir.wrapping_key_id.is_none() {
683 self.fsck.error(FsckError::UnencryptedDirectoryHasEncryptedChild(
684 self.store_id,
685 parent_id,
686 child_id,
687 ))?;
688 }
689 if let Some(crypt) = &self.crypt
690 && let Some(key) = &dir.fscrypt_key
691 && !matches!(key, WrappedKey::Fxfs(_))
692 {
693 if let Ok(unwrapped_key) = crypt.unwrap_key(key, parent_id).await {
695 let cipher = key_to_cipher(key, &unwrapped_key)?;
696 match decrypt_filename(cipher.as_ref(), parent_id, &name) {
697 Ok(name) => {
698 if cipher.hash_code_casefold(&name) != *hash_code {
699 self.fsck.error(FsckError::BadCasefoldHash(
700 self.store_id,
701 parent_id,
702 child_id,
703 ))?;
704 }
705 }
706 Err(_) => {
707 }
709 }
710 }
711 }
712 }
713 ObjectKeyData::EncryptedChild(_) => {
714 if dir.wrapping_key_id.is_none() {
715 self.fsck.error(FsckError::UnencryptedDirectoryHasEncryptedChild(
716 self.store_id,
717 parent_id,
718 child_id,
719 ))?;
720 }
721 }
722 _ => {
723 bail!(
724 "Unexpected object_key_data type in process_child: {:?}",
725 object_key_data
726 );
727 }
728 };
729 }
730 match (self.objects.get_mut(&child_id), object_descriptor) {
731 (
732 Some(ScannedObject::File(ScannedFile { parents, .. })),
733 ObjectDescriptor::File | ObjectDescriptor::Volume,
734 ) => {
735 parents.push(parent_id);
736 }
737 (
738 Some(ScannedObject::Directory(ScannedDir { parent, wrapping_key_id, .. })),
739 ObjectDescriptor::Directory,
740 ) => {
741 if matches!(
742 object_key_data,
743 ObjectKeyData::EncryptedChild(_) | ObjectKeyData::EncryptedCasefoldChild(_)
744 ) {
745 if let Some(id) = wrapping_key_id {
746 child_wrapping_key_id = Some(*id);
747 } else {
748 self.fsck.error(FsckError::EncryptedChildDirectoryNoWrappingKey(
749 self.store_id,
750 child_id,
751 ))?;
752 }
753 }
754 if parent.is_some() {
755 self.fsck
758 .error(FsckError::MultipleLinksToDirectory(self.store_id, child_id))?;
759 }
760 *parent = Some(parent_id);
761 }
762 (Some(ScannedObject::Tombstone), _) => {
763 self.fsck.error(FsckError::TombstonedObjectHasRecords(self.store_id, parent_id))?;
764 return Ok(());
765 }
766 (None, _) => {
767 self.fsck.error(FsckError::MissingObjectInfo(self.store_id, child_id))?;
768 return Ok(());
769 }
770 (Some(s), _) => {
771 let expected = match s {
772 ScannedObject::Directory(_) => ObjectDescriptor::Directory,
773 ScannedObject::File(_) | ScannedObject::Graveyard => ObjectDescriptor::File,
774 ScannedObject::Symlink(ScannedSymlink { parents, .. }) => {
775 parents.push(parent_id);
776 ObjectDescriptor::Symlink
777 }
778 ScannedObject::Tombstone => unreachable!(),
779 };
780 if &expected != object_descriptor {
781 self.fsck.error(FsckError::ConflictingTypeForLink(
782 self.store_id,
783 child_id,
784 expected.into(),
785 object_descriptor.into(),
786 ))?;
787 }
788 }
789 }
790 match self.objects.get_mut(&parent_id) {
791 Some(
792 ScannedObject::File(..) | ScannedObject::Graveyard | ScannedObject::Symlink(_),
793 ) => {
794 self.fsck.error(FsckError::ObjectHasChildren(self.store_id, parent_id))?;
795 }
796 Some(ScannedObject::Directory(ScannedDir {
797 observed_sub_dirs,
798 wrapping_key_id,
799 ..
800 })) => {
801 if let Some(parent_wrapping_key_id) = *wrapping_key_id {
802 if !matches!(
803 object_key_data,
804 ObjectKeyData::EncryptedChild(_) | ObjectKeyData::EncryptedCasefoldChild(_)
805 ) {
806 self.fsck.error(FsckError::EncryptedDirectoryHasUnencryptedChild(
807 self.store_id,
808 parent_id,
809 child_id,
810 ))?;
811 } else {
812 if let Some(child_wrapping_key_id) = child_wrapping_key_id {
813 if child_wrapping_key_id != parent_wrapping_key_id {
814 self.fsck.error(
815 FsckError::ChildEncryptedWithDifferentWrappingKeyThanParent(
816 self.store_id,
817 parent_id,
818 child_id,
819 parent_wrapping_key_id,
820 child_wrapping_key_id,
821 ),
822 )?;
823 }
824 }
825 }
826 }
827 if *object_descriptor == ObjectDescriptor::Directory {
828 *observed_sub_dirs += 1;
829 }
830 }
831 Some(ScannedObject::Tombstone) => {
832 self.fsck.error(FsckError::TombstonedObjectHasRecords(self.store_id, parent_id))?;
833 }
834 None => self.fsck.error(FsckError::MissingObjectInfo(self.store_id, parent_id))?,
835 }
836 Ok(())
837 }
838
839 fn process_extent(
841 &mut self,
842 object_id: u64,
843 attribute_id: u64,
844 range: &Range<u64>,
845 device_offset: u64,
846 bs: u64,
847 is_overwrite_extent: bool,
848 ) -> Result<(), Error> {
849 if range.start % bs > 0 || range.end % bs > 0 {
850 self.fsck.error(FsckError::MisalignedExtent(
851 self.store_id,
852 object_id,
853 range.clone(),
854 0,
855 ))?;
856 }
857 if range.start >= range.end {
858 self.fsck.error(FsckError::MalformedExtent(
859 self.store_id,
860 object_id,
861 range.clone(),
862 0,
863 ))?;
864 return Ok(());
865 }
866 let len = range.end - range.start;
867 match self.objects.get_mut(&object_id) {
868 Some(
869 ScannedObject::File(ScannedFile { attributes, .. })
870 | ScannedObject::Directory(ScannedDir { attributes, .. })
871 | ScannedObject::Symlink(ScannedSymlink { attributes, .. }),
872 ) => {
873 let ScannedAttributes {
874 attributes,
875 tombstoned_attributes,
876 observed_allocated_size: allocated_size,
877 in_graveyard,
878 ..
879 } = attributes;
880 match attributes.iter_mut().find(|attribute| attribute.attribute_id == attribute_id)
881 {
882 Some(attribute) => {
883 if !*in_graveyard
884 && !tombstoned_attributes.contains(&attribute_id)
885 && range.end > round_up(attribute.size, bs).unwrap()
886 {
887 self.fsck.error(FsckError::ExtentExceedsLength(
888 self.store_id,
889 object_id,
890 attribute_id,
891 attribute.size,
892 range.into(),
893 ))?;
894 }
895 attribute.observed_overwrite_extents =
896 attribute.observed_overwrite_extents || is_overwrite_extent;
897 }
898 None => {
899 self.fsck.warning(FsckWarning::ExtentForMissingAttribute(
900 self.store_id,
901 object_id,
902 attribute_id,
903 ))?;
904 }
905 }
906 *allocated_size += len;
907 }
908 Some(ScannedObject::Graveyard | ScannedObject::Tombstone) => { }
909 None => {
910 self.fsck
911 .warning(FsckWarning::ExtentForNonexistentObject(self.store_id, object_id))?;
912 }
913 }
914 if device_offset % bs > 0 {
915 self.fsck.error(FsckError::MisalignedExtent(
916 self.store_id,
917 object_id,
918 range.clone(),
919 device_offset,
920 ))?;
921 }
922 let item = Item::new(
923 AllocatorKey { device_range: device_offset..device_offset + len },
924 AllocatorValue::Abs { count: 1, owner_object_id: self.store_id },
925 );
926 let lower_bound: AllocatorKey = item.key.lower_bound_for_merge_into();
927 self.fsck.allocations.merge_into(item, &lower_bound, allocator::merge::merge);
928 Ok(())
929 }
930
931 fn handle_graveyard_entry(
933 &mut self,
934 object_id: u64,
935 attribute_id: Option<u64>,
936 tombstone: bool,
937 ) -> Result<(), Error> {
938 match self.objects.get_mut(&object_id) {
939 Some(ScannedObject::File(ScannedFile {
940 parents,
941 attributes: ScannedAttributes { in_graveyard, tombstoned_attributes, .. },
942 ..
943 })) => {
944 if attribute_id.is_none() {
945 *in_graveyard = true;
946 }
947 if tombstone {
948 if let Some(attribute_id) = attribute_id {
949 tombstoned_attributes.push(attribute_id);
950 } else {
951 parents.push(INVALID_OBJECT_ID)
952 }
953 }
954 }
955 Some(
956 ScannedObject::Directory(ScannedDir { attributes, .. })
957 | ScannedObject::Symlink(ScannedSymlink { attributes, .. }),
958 ) => {
959 if tombstone {
960 if let Some(attribute_id) = attribute_id {
961 attributes.tombstoned_attributes.push(attribute_id);
962 } else {
963 attributes.in_graveyard = true;
964 }
965 } else {
966 self.fsck.error(FsckError::UnexpectedObjectInGraveyard(object_id))?;
967 }
968 }
969 Some(ScannedObject::Graveyard | ScannedObject::Tombstone) => {
970 self.fsck.error(FsckError::UnexpectedObjectInGraveyard(object_id))?;
971 }
972 None => {
973 self.fsck.warning(FsckWarning::GraveyardRecordForAbsentObject(
974 self.store_id,
975 object_id,
976 ))?;
977 }
978 }
979 Ok(())
980 }
981
982 fn finish_file(&mut self) -> Result<(), Error> {
984 if let Some(current_file) = self.current_object.take() {
985 if self.is_encrypted {
986 let mut key_ids = vec![];
987 for id in current_file.key_ids.iter() {
988 key_ids.push(*id);
989 }
990
991 if key_ids.is_empty() && !current_file.lazy_keys {
997 self.fsck.error(FsckError::MissingEncryptionKeys(
998 self.store_id,
999 current_file.object_id,
1000 ))?;
1001 }
1002
1003 match self.objects.get_mut(¤t_file.object_id) {
1004 Some(ScannedObject::Directory(ScannedDir {
1005 wrapping_key_id,
1006 attributes,
1007 ..
1008 })) => {
1009 if !attributes.extended_attributes.is_empty() {
1010 if !key_ids.contains(&VOLUME_DATA_KEY_ID) {
1011 self.fsck.error(FsckError::MissingKey(
1012 self.store_id,
1013 current_file.object_id,
1014 VOLUME_DATA_KEY_ID,
1015 ))?;
1016 }
1017 }
1018 if wrapping_key_id.is_some() {
1019 if !key_ids.contains(&FSCRYPT_KEY_ID) {
1020 self.fsck.error(FsckError::MissingKey(
1021 self.store_id,
1022 current_file.object_id,
1023 FSCRYPT_KEY_ID,
1024 ))?;
1025 }
1026 }
1027 }
1028 Some(ScannedObject::Symlink(ScannedSymlink { encrypted, .. })) => {
1029 if *encrypted {
1030 if !key_ids.contains(&FSCRYPT_KEY_ID) {
1031 self.fsck.error(FsckError::MissingKey(
1032 self.store_id,
1033 current_file.object_id,
1034 FSCRYPT_KEY_ID,
1035 ))?;
1036 }
1037 }
1038 }
1039 Some(_) => {}
1040 None => self.fsck.error(FsckError::MissingObjectInfo(
1041 self.store_id,
1042 current_file.object_id,
1043 ))?,
1044 }
1045 }
1046 }
1047 Ok(())
1048 }
1049}
1050
1051async fn scan_extents_and_directory_children<'a>(
1056 store: &ObjectStore,
1057 scanned: &mut ScannedStore<'a>,
1058 result: &mut FsckResult,
1059) -> Result<(), Error> {
1060 let bs = store.block_size();
1061 let layer_set = store.tree().layer_set();
1062 let mut merger = layer_set.merger();
1063 let mut iter = merger.query(Query::FullScan).await?;
1064 let mut allocated_bytes = 0;
1065 let mut extent_count = 0;
1066 let mut previous_object_id = INVALID_OBJECT_ID;
1067 while let Some(itemref) = iter.get() {
1068 match itemref {
1069 ItemRef {
1070 key:
1071 ObjectKey {
1072 object_id,
1073 data:
1074 ObjectKeyData::Attribute(
1075 attribute_id,
1076 AttributeKey::Extent(ExtentKey { range }),
1077 ),
1078 },
1079 value: ObjectValue::Extent(extent),
1080 ..
1081 } => {
1082 if let ExtentValue::Some { device_offset, mode, .. } = extent {
1084 let size = range.length().unwrap_or(0);
1085 allocated_bytes += size;
1086
1087 if previous_object_id != *object_id {
1088 if previous_object_id != INVALID_OBJECT_ID {
1089 result.fragmentation.extent_count
1090 [FragmentationStats::get_histogram_bucket_for_count(
1091 extent_count,
1092 )] += 1;
1093 }
1094 extent_count = 0;
1095 previous_object_id = *object_id;
1096 }
1097 result.fragmentation.extent_size
1098 [FragmentationStats::get_histogram_bucket_for_size(size)] += 1;
1099 extent_count += 1;
1100
1101 let is_overwrite_extent =
1102 matches!(mode, ExtentMode::Overwrite | ExtentMode::OverwritePartial(_));
1103
1104 scanned.process_extent(
1105 *object_id,
1106 *attribute_id,
1107 range,
1108 *device_offset,
1109 bs,
1110 is_overwrite_extent,
1111 )?;
1112 };
1113 }
1114 ItemRef {
1115 key:
1116 ObjectKey {
1117 object_id,
1118 data:
1119 object_key_data @ (ObjectKeyData::Child { .. }
1120 | ObjectKeyData::EncryptedChild(_)
1121 | ObjectKeyData::CasefoldChild { .. }
1122 | ObjectKeyData::EncryptedCasefoldChild { .. }),
1123 },
1124 value: ObjectValue::Child(ChildValue { object_id: child_id, object_descriptor }),
1125 ..
1126 } => {
1127 scanned
1128 .process_child(*object_id, *child_id, object_descriptor, object_key_data)
1129 .await?
1130 }
1131 _ => {}
1132 }
1133 iter.advance().await?;
1134 }
1135 if extent_count != 0 && previous_object_id != INVALID_OBJECT_ID {
1136 result.fragmentation.extent_count
1137 [FragmentationStats::get_histogram_bucket_for_count(extent_count)] += 1;
1138 }
1139 scanned.fsck.verbose(format!(
1140 "Store {} has {} bytes allocated",
1141 store.store_object_id(),
1142 allocated_bytes
1143 ));
1144 Ok(())
1145}
1146
1147fn validate_attributes(
1148 fsck: &Fsck<'_>,
1149 store_id: u64,
1150 object_id: u64,
1151 attributes: &ScannedAttributes,
1152 is_file: bool,
1153 verified: VerifiedType,
1154 block_size: u64,
1155) -> Result<(), Error> {
1156 let ScannedAttributes {
1157 attributes,
1158 tombstoned_attributes,
1159 observed_allocated_size,
1160 stored_allocated_size,
1161 extended_attributes,
1162 ..
1163 } = attributes;
1164 if observed_allocated_size != stored_allocated_size {
1165 fsck.error(FsckError::AllocatedSizeMismatch(
1166 store_id,
1167 object_id,
1168 *observed_allocated_size,
1169 *stored_allocated_size,
1170 ))?;
1171 }
1172
1173 if is_file {
1174 let data_attribute =
1175 attributes.iter().find(|attribute| attribute.attribute_id == DEFAULT_DATA_ATTRIBUTE_ID);
1176 match data_attribute {
1177 None => fsck.error(FsckError::MissingDataAttribute(store_id, object_id))?,
1178 Some(data_attribute) => {
1179 let merkle_attribute = attributes
1180 .iter()
1181 .find(|attribute| attribute.attribute_id == FSVERITY_MERKLE_ATTRIBUTE_ID);
1182
1183 if !matches!(verified, VerifiedType::None) && merkle_attribute.is_none() {
1187 fsck.error(FsckError::VerifiedFileDoesNotHaveAMerkleAttribute(
1188 store_id, object_id,
1189 ))?;
1190 }
1191
1192 match verified {
1193 VerifiedType::None => {}
1194 VerifiedType::Internal(hash_size) => {
1195 if let Some(merkle_attribute) = merkle_attribute {
1196 let expected_size = if data_attribute.size == 0 {
1197 hash_size as u64
1198 } else {
1201 ((data_attribute.size + (block_size - 1)) / block_size)
1202 * hash_size as u64
1203 };
1204 if merkle_attribute.size != expected_size {
1205 fsck.error(FsckError::IncorrectMerkleTreeSize(
1206 store_id,
1207 object_id,
1208 expected_size,
1209 merkle_attribute.size,
1210 ))?;
1211 }
1212 } else {
1213 fsck.error(FsckError::VerifiedFileDoesNotHaveAMerkleAttribute(
1214 store_id, object_id,
1215 ))?;
1216 }
1217 }
1218 VerifiedType::F2fs(attr_end) => {
1219 match merkle_attribute {
1220 Some(merkle_attribute) => {
1224 if merkle_attribute.size != attr_end {
1225 fsck.error(FsckError::IncorrectMerkleTreeSize(
1226 store_id,
1227 object_id,
1228 attr_end,
1229 merkle_attribute.size,
1230 ))?;
1231 }
1232 }
1233 None => {
1234 fsck.error(FsckError::VerifiedFileDoesNotHaveAMerkleAttribute(
1235 store_id, object_id,
1236 ))?;
1237 }
1238 }
1239 }
1240 }
1241 }
1242 }
1243 }
1244
1245 for attr in tombstoned_attributes {
1247 if attributes.iter().find(|attribute| attribute.attribute_id == *attr).is_none() {
1248 fsck.error(FsckError::TombstonedAttributeDoesNotExist(store_id, object_id, *attr))?
1249 }
1250 }
1251
1252 for expected_attribute_id in extended_attributes {
1253 if attributes
1254 .iter()
1255 .find(|attribute| attribute.attribute_id == *expected_attribute_id)
1256 .is_none()
1257 {
1258 fsck.error(FsckError::MissingAttributeForExtendedAttribute(
1259 store_id,
1260 object_id,
1261 *expected_attribute_id,
1262 ))?;
1263 }
1264 }
1265
1266 for attribute in attributes {
1267 if attribute.attribute_id >= EXTENDED_ATTRIBUTE_RANGE_START
1268 && attribute.attribute_id < EXTENDED_ATTRIBUTE_RANGE_END
1269 {
1270 if extended_attributes
1273 .iter()
1274 .find(|xattr_id| attribute.attribute_id == **xattr_id)
1275 .is_none()
1276 {
1277 fsck.warning(FsckWarning::OrphanedExtendedAttribute(
1278 store_id,
1279 object_id,
1280 attribute.attribute_id,
1281 ))?;
1282 }
1283 }
1284
1285 if attribute
1286 .has_overwrite_extents_flag
1287 .is_some_and(|has_flag| has_flag && !attribute.observed_overwrite_extents)
1288 {
1289 fsck.error(FsckError::MissingOverwriteExtents(
1290 store_id,
1291 object_id,
1292 attribute.attribute_id,
1293 ))?;
1294 }
1295 if attribute
1296 .has_overwrite_extents_flag
1297 .is_some_and(|has_flag| !has_flag && attribute.observed_overwrite_extents)
1298 {
1299 fsck.error(FsckError::OverwriteExtentFlagUnset(
1300 store_id,
1301 object_id,
1302 attribute.attribute_id,
1303 ))?;
1304 }
1305 }
1306
1307 Ok(())
1308}
1309
1310pub(super) async fn scan_store(
1313 fsck: &Fsck<'_>,
1314 store: &ObjectStore,
1315 root_objects: impl AsRef<[u64]>,
1316 result: &mut FsckResult,
1317) -> Result<(), Error> {
1318 let store_id = store.store_object_id();
1319 let next_object_id = store.query_next_object_id();
1320
1321 let mut scanned = ScannedStore::new(
1322 fsck,
1323 store.crypt(),
1324 root_objects,
1325 store_id,
1326 store.is_root(),
1327 store.is_encrypted(),
1328 store.root_directory_object_id(),
1329 );
1330
1331 let layer_set = store.tree().layer_set();
1333 let mut merger = layer_set.merger();
1334 let mut iter = merger.query(Query::FullScan).await?;
1335 let mut last_item: Option<Item<ObjectKey, ObjectValue>> = None;
1336 while let Some(item) = iter.get() {
1337 if let Some(last_item) = last_item {
1338 if last_item.key >= *item.key {
1339 fsck.fatal(FsckFatal::MisOrderedObjectStore(store_id))?;
1340 }
1341 }
1342 if item.key.object_id == INVALID_OBJECT_ID {
1343 fsck.warning(FsckWarning::InvalidObjectIdInStore(
1344 store_id,
1345 item.key.into(),
1346 item.value.into(),
1347 ))?;
1348 }
1349 if next_object_id != INVALID_OBJECT_ID && item.key.object_id == next_object_id {
1350 fsck.error(FsckError::NextObjectIdInUse(store_id, next_object_id))?;
1351 }
1352 if let Some(current_file) = &scanned.current_object {
1353 if item.key.object_id != current_file.object_id {
1354 scanned.finish_file()?;
1355 }
1356 }
1357 scanned.process(item.key, item.value)?;
1358 last_item = Some(item.cloned());
1359 iter.advance().await?;
1360 }
1361 scanned.finish_file()?;
1362
1363 for (project_id, node_id) in scanned.used_project_ids.iter() {
1364 if !scanned.stored_project_usages.contains_key(project_id) {
1365 fsck.error(FsckError::ProjectUsedWithNoUsageTracking(store_id, *project_id, *node_id))?;
1366 }
1367 }
1368 for (project_id, (bytes_stored, nodes_stored)) in scanned.stored_project_usages.iter() {
1369 if let Some((bytes_used, nodes_used)) = scanned.total_project_usages.get(&project_id) {
1370 if *bytes_stored != *bytes_used || *nodes_stored != *nodes_used {
1371 fsck.warning(FsckWarning::ProjectUsageInconsistent(
1372 store_id,
1373 *project_id,
1374 (*bytes_stored, *nodes_stored),
1375 (*bytes_used, *nodes_used),
1376 ))?;
1377 }
1378 } else {
1379 if *bytes_stored > 0 || *nodes_stored > 0 {
1380 fsck.warning(FsckWarning::ProjectUsageInconsistent(
1381 store_id,
1382 *project_id,
1383 (*bytes_stored, *nodes_stored),
1384 (0, 0),
1385 ))?;
1386 }
1387 }
1388 }
1389
1390 let layer_set = store.tree().layer_set();
1394 let mut merger = layer_set.merger();
1395 let mut iter = fsck.assert(
1396 Graveyard::iter(store.graveyard_directory_object_id(), &mut merger).await,
1397 FsckFatal::MalformedGraveyard,
1398 )?;
1399 while let Some(info) = iter.get() {
1400 match info.value() {
1401 ObjectValue::Some => {
1402 scanned.handle_graveyard_entry(info.object_id(), info.attribute_id(), true)?
1403 }
1404 ObjectValue::Trim => {
1405 if let Some(attribute_id) = info.attribute_id() {
1406 fsck.error(FsckError::TrimValueForGraveyardAttributeEntry(
1407 store_id,
1408 info.object_id(),
1409 attribute_id,
1410 ))?
1411 } else {
1412 scanned.handle_graveyard_entry(info.object_id(), None, false)?
1413 }
1414 }
1415 _ => fsck.error(FsckError::BadGraveyardValue(store_id, info.object_id()))?,
1416 }
1417 fsck.assert(iter.advance().await, FsckFatal::MalformedGraveyard)?;
1418 }
1419
1420 scan_extents_and_directory_children(store, &mut scanned, result).await?;
1421
1422 for oid in scanned.root_objects {
1426 if let Some(ScannedObject::Directory(ScannedDir { visited, .. })) =
1427 scanned.objects.get_mut(&oid)
1428 {
1429 *visited.get_mut() = true;
1430 }
1431 }
1432
1433 let mut num_objects = 0;
1435 let mut files = 0;
1436 let mut directories = 0;
1437 let mut symlinks = 0;
1438 let mut tombstones = 0;
1439 let mut other = 0;
1440 let mut stack = Vec::new();
1441 for (object_id, object) in &scanned.objects {
1442 num_objects += 1;
1443 match object {
1444 ScannedObject::File(ScannedFile {
1445 parents,
1446 stored_refs,
1447 attributes,
1448 verified: is_verified,
1449 ..
1450 }) => {
1451 files += 1;
1452 let observed_refs = parents.len().try_into().unwrap();
1453 if observed_refs != *stored_refs && observed_refs > 0 {
1455 fsck.error(FsckError::RefCountMismatch(
1456 *object_id,
1457 observed_refs,
1458 *stored_refs,
1459 ))?;
1460 }
1461 validate_attributes(
1462 fsck,
1463 store_id,
1464 *object_id,
1465 attributes,
1466 true,
1467 is_verified.clone(),
1468 store.block_size(),
1469 )?;
1470 if parents.is_empty() {
1471 fsck.warning(FsckWarning::OrphanedObject(store_id, *object_id))?;
1472 }
1473 if parents.contains(&INVALID_OBJECT_ID) && parents.len() > 1 {
1474 let parents = parents
1475 .iter()
1476 .filter(|oid| **oid != INVALID_OBJECT_ID)
1477 .cloned()
1478 .collect::<Vec<u64>>();
1479 fsck.error(FsckError::ZombieFile(store_id, *object_id, parents))?;
1480 }
1481 }
1482 ScannedObject::Directory(ScannedDir {
1483 stored_sub_dirs,
1484 observed_sub_dirs,
1485 parent,
1486 visited,
1487 attributes,
1488 ..
1489 }) => {
1490 directories += 1;
1491 if *observed_sub_dirs != *stored_sub_dirs {
1492 fsck.error(FsckError::SubDirCountMismatch(
1493 store_id,
1494 *object_id,
1495 *observed_sub_dirs,
1496 *stored_sub_dirs,
1497 ))?;
1498 }
1499 validate_attributes(
1500 fsck,
1501 store_id,
1502 *object_id,
1503 attributes,
1504 false,
1505 VerifiedType::None,
1506 store.block_size(),
1507 )?;
1508 if let &Some(mut oid) = parent {
1509 if attributes.in_graveyard {
1510 fsck.error(FsckError::ZombieDir(store_id, *object_id, oid))?;
1511 }
1512 if !std::mem::replace(unsafe { &mut *visited.get() }, true) {
1516 stack.push(*object_id);
1517 loop {
1518 if let Some(ScannedObject::Directory(ScannedDir {
1519 parent: Some(parent),
1520 visited,
1521 ..
1522 })) = scanned.objects.get(&oid)
1523 {
1524 stack.push(oid);
1525 oid = *parent;
1526 if std::mem::replace(unsafe { &mut *visited.get() }, true) {
1528 break;
1529 }
1530 } else {
1531 break;
1534 }
1535 }
1536 for s in stack.drain(..) {
1539 if s == oid {
1540 fsck.error(FsckError::LinkCycle(store_id, oid))?;
1541 break;
1542 }
1543 }
1544 }
1545 } else if !attributes.in_graveyard {
1546 fsck.warning(FsckWarning::OrphanedObject(store_id, *object_id))?;
1547 }
1548 }
1549 ScannedObject::Graveyard => other += 1,
1550 ScannedObject::Symlink(ScannedSymlink { parents, stored_refs, attributes, .. }) => {
1551 symlinks += 1;
1552 let observed_refs = parents.len().try_into().unwrap();
1553 if observed_refs != *stored_refs && observed_refs > 0 {
1555 fsck.error(FsckError::RefCountMismatch(
1556 *object_id,
1557 observed_refs,
1558 *stored_refs,
1559 ))?;
1560 }
1561 validate_attributes(
1562 fsck,
1563 store_id,
1564 *object_id,
1565 attributes,
1566 false,
1567 VerifiedType::None,
1568 store.block_size(),
1569 )?;
1570 if attributes.in_graveyard {
1571 if !parents.is_empty() {
1572 fsck.error(FsckError::ZombieSymlink(
1573 store_id,
1574 *object_id,
1575 parents.clone(),
1576 ))?;
1577 }
1578 } else if parents.is_empty() {
1579 fsck.warning(FsckWarning::OrphanedObject(store_id, *object_id))?;
1580 }
1581 }
1582 ScannedObject::Tombstone => {
1583 tombstones += 1;
1584 num_objects -= 1;
1585 }
1586 }
1587 }
1588 if num_objects != store.object_count() {
1589 fsck.error(FsckError::ObjectCountMismatch(store_id, num_objects, store.object_count()))?;
1590 }
1591 fsck.verbose(format!(
1592 "Store {store_id} has {files} files, {directories} dirs, {symlinks} symlinks, \
1593 {tombstones} tombstones, {other} other objects",
1594 ));
1595
1596 Ok(())
1597}