55//! verification and garbage collection support.
66
77use std:: {
8- collections:: HashSet ,
9- ffi:: CStr ,
8+ collections:: { HashMap , HashSet } ,
9+ ffi:: { CStr , CString , OsStr } ,
1010 fs:: { canonicalize, File } ,
1111 io:: { Read , Write } ,
1212 os:: fd:: { AsFd , OwnedFd } ,
13+ os:: unix:: ffi:: OsStrExt ,
1314 path:: { Path , PathBuf } ,
1415 sync:: Arc ,
1516} ;
@@ -572,7 +573,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
572573 Ok ( ObjectID :: from_object_pathname ( link_content. to_bytes ( ) ) ?)
573574 }
574575
575- fn walk_symlinkdir ( fd : OwnedFd , objects : & mut HashSet < ObjectID > ) -> Result < ( ) > {
576+ fn walk_symlinkdir ( fd : OwnedFd , entry_digests : & mut HashSet < CString > ) -> Result < ( ) > {
576577 for item in Dir :: read_from ( & fd) ? {
577578 let entry = item?;
578579 // NB: the underlying filesystem must support returning filetype via direntry
@@ -582,11 +583,18 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
582583 let filename = entry. file_name ( ) ;
583584 if filename != c"." && filename != c".." {
584585 let dirfd = openat ( & fd, filename, OFlags :: RDONLY , Mode :: empty ( ) ) ?;
585- Self :: walk_symlinkdir ( dirfd, objects ) ?;
586+ Self :: walk_symlinkdir ( dirfd, entry_digests ) ?;
586587 }
587588 }
588589 FileType :: Symlink => {
589- objects. insert ( Self :: read_symlink_hashvalue ( & fd, entry. file_name ( ) ) ?) ;
590+ let link_content = readlinkat ( & fd, entry. file_name ( ) , [ ] ) ?;
591+ let linked_path = Path :: new ( OsStr :: from_bytes ( link_content. as_bytes ( ) ) ) ;
592+ if let Some ( entry_name) = linked_path. file_name ( ) {
593+ entry_digests. insert ( CString :: new ( entry_name. as_bytes ( ) ) ?) ;
594+ } else {
595+ // Does not have a proper file base name (i.e. "..")
596+ continue ;
597+ }
590598 }
591599 _ => {
592600 bail ! ( "Unexpected file type encountered" ) ;
@@ -608,53 +616,96 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
608616 )
609617 }
610618
611- fn gc_category ( & self , category : & str ) -> Result < HashSet < ObjectID > > {
612- let mut objects = HashSet :: new ( ) ;
613-
619+ // For a GC category (images / streams), return underlying entry digests and
620+ // object IDs for each entry, as a reverse lookup map
621+ // If refs_only == false, all entries will be returned, and only orphans in
622+ // objects/ will be GC'd
623+ // If refs_only == true, only entries explicitly referred to in <category>/refs
624+ // will be returned, allow unlinked entries to be GC'ed
625+ fn gc_category ( & self , category : & str , refs_only : bool ) -> Result < HashMap < ObjectID , String > > {
614626 let Some ( category_fd) = self
615627 . openat ( category, OFlags :: RDONLY | OFlags :: DIRECTORY )
616628 . filter_errno ( Errno :: NOENT )
617629 . context ( "Opening {category} dir in repository" ) ?
618630 else {
619- return Ok ( objects ) ;
631+ return Ok ( HashMap :: new ( ) ) ;
620632 } ;
621633
622- if let Some ( refs) = openat (
623- & category_fd,
624- "refs" ,
625- OFlags :: RDONLY | OFlags :: DIRECTORY ,
626- Mode :: empty ( ) ,
627- )
628- . filter_errno ( Errno :: NOENT )
629- . context ( "Opening {category}/refs dir in repository" ) ?
630- {
631- Self :: walk_symlinkdir ( refs, & mut objects) ?;
632- }
633-
634- for item in Dir :: read_from ( & category_fd) ? {
635- let entry = item?;
636- let filename = entry. file_name ( ) ;
637- if filename != c"refs" && filename != c"." && filename != c".." {
638- if entry. file_type ( ) != FileType :: Symlink {
639- bail ! ( "category directory contains non-symlink" ) ;
634+ let mut entry_digests = HashSet :: new ( ) ;
635+ if refs_only {
636+ if let Some ( refs) = openat (
637+ & category_fd,
638+ "refs" ,
639+ OFlags :: RDONLY | OFlags :: DIRECTORY ,
640+ Mode :: empty ( ) ,
641+ )
642+ . filter_errno ( Errno :: NOENT )
643+ . context ( "Opening {category}/refs dir in repository" ) ?
644+ {
645+ Self :: walk_symlinkdir ( refs, & mut entry_digests) ?;
646+ }
647+ } else {
648+ // All first-level link entries should be directly object references
649+ for item in Dir :: read_from ( & category_fd) ? {
650+ let entry = item?;
651+ let filename = entry. file_name ( ) ;
652+ if filename != c"refs" && filename != c"." && filename != c".." {
653+ if entry. file_type ( ) != FileType :: Symlink {
654+ bail ! ( "category directory contains non-symlink" ) ;
655+ }
656+ entry_digests. insert ( entry. file_name ( ) . to_owned ( ) ) ;
640657 }
658+ }
659+ }
641660
642- // TODO: we need to sort this out. the symlink itself might be a sha256 content ID
643- // (as for splitstreams), not an object/ to be preserved.
644- continue ;
661+ let objects = entry_digests
662+ . into_iter ( )
663+ . map ( |entry_fn| {
664+ Ok ( (
665+ Self :: read_symlink_hashvalue ( & category_fd, & entry_fn) ?,
666+ entry_fn. to_str ( ) ?. to_owned ( ) ,
667+ ) )
668+ } )
669+ . collect :: < Result < _ > > ( ) ?;
645670
646- /*
647- let mut value = Sha256HashValue::EMPTY;
648- hex::decode_to_slice(filename.to_bytes(), &mut value)?;
671+ Ok ( objects)
672+ }
649673
650- if !objects.contains(&value) {
651- println!("rm {}/{:?}", category, filename);
652- }
653- */
654- }
674+ // Traverse split streams to resolve all linked objects
675+ fn walk_streams (
676+ & self ,
677+ stream_digest : & str ,
678+ walked_streams : & mut HashSet < String > ,
679+ objects : & mut HashSet < ObjectID > ,
680+ stream_map : & HashMap < ObjectID , String > ,
681+ ) -> Result < ( ) > {
682+ // A split stream links to stored objects, but the the linked objects themselves could be streams that links
683+ // to other objects as well. For example an OCI image manifest links to layer objects which are split streams
684+ // linking to layer contents. This function walks the streams down and collects all linked objects
685+ if walked_streams. contains ( stream_digest) {
686+ return Ok ( ( ) ) ;
655687 }
688+ walked_streams. insert ( stream_digest. to_owned ( ) ) ;
689+
690+ let mut split_stream = self . open_stream ( stream_digest, None ) ?;
691+ let mut streams_to_walk = HashSet :: new ( ) ;
692+ split_stream. get_object_refs ( |id| {
693+ println ! ( "# with {id:?}" ) ;
694+ objects. insert ( id. clone ( ) ) ;
695+ if stream_map. contains_key ( id) {
696+ let digest = stream_map. get ( id) . expect ( "key exists" ) ;
697+ println ! ( "# which is streams/{digest:}" ) ;
698+ streams_to_walk. insert ( digest) ;
699+ }
700+ } ) ?;
701+ streams_to_walk
702+ . into_iter ( )
703+ . map ( |stream_digest| {
704+ self . walk_streams ( stream_digest, walked_streams, objects, stream_map)
705+ } )
706+ . collect :: < Result < ( ) > > ( ) ?;
656707
657- Ok ( objects )
708+ Ok ( ( ) )
658709 }
659710
660711 /// Given an image, return the set of all objects referenced by it.
@@ -675,21 +726,21 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
675726
676727 let mut objects = HashSet :: new ( ) ;
677728
678- for ref object in self . gc_category ( "images" ) ? {
679- println ! ( "{object:?} lives as an image" ) ;
680- objects. insert ( object. clone ( ) ) ;
681- objects. extend ( self . objects_for_image ( & object. to_hex ( ) ) ?) ;
729+ for ref image in self . gc_category ( "images" , true ) ? {
730+ println ! ( "# {image:?} lives as an image" ) ;
731+ objects. insert ( image. 0 . clone ( ) ) ;
732+ self . objects_for_image ( & image. 1 ) ?. iter ( ) . for_each ( |id| {
733+ println ! ( "# with {id:?}" ) ;
734+ objects. insert ( id. clone ( ) ) ;
735+ } ) ;
682736 }
683737
684- for object in self . gc_category ( "streams" ) ? {
685- println ! ( "{object:?} lives as a stream" ) ;
686- objects. insert ( object. clone ( ) ) ;
687-
688- let mut split_stream = self . open_stream ( & object. to_hex ( ) , None ) ?;
689- split_stream. get_object_refs ( |id| {
690- println ! ( " with {id:?}" ) ;
691- objects. insert ( id. clone ( ) ) ;
692- } ) ?;
738+ let mut walked_streams = HashSet :: new ( ) ;
739+ let stream_map = self . gc_category ( "streams" , false ) ?;
740+ for stream in self . gc_category ( "streams" , true ) ? {
741+ println ! ( "# {stream:?} lives as a stream" ) ;
742+ objects. insert ( stream. 0 . clone ( ) ) ;
743+ self . walk_streams ( & stream. 1 , & mut walked_streams, & mut objects, & stream_map) ?;
693744 }
694745
695746 for first_byte in 0x0 ..=0xff {
0 commit comments