1use crate::dir::{ShardError, UnexpectedDirectoryProperties};
2use crate::file::visit::{Cache, FileVisit, IdleFileVisit};
3use crate::file::{FileError, FileReadFailed};
4use crate::pb::{FlatUnixFs, PBLink, ParsingFailed, UnixFsType};
5use crate::{InvalidCidInLink, Metadata, UnexpectedNodeType};
6use alloc::borrow::Cow;
7use cid::Cid;
8use core::convert::TryFrom;
9use core::fmt;
10use either::Either;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug)]
17pub struct Walker {
18 current: Option<InnerEntry>,
21 next: Option<(Cid, String, usize)>,
25 pending: Vec<(Cid, String, usize)>,
26 should_continue: bool,
29}
30
31fn convert_link(
33 nested_depth: usize,
34 nth: usize,
35 link: PBLink<'_>,
36) -> Result<(Cid, String, usize), InvalidCidInLink> {
37 let hash = link.Hash.as_deref().unwrap_or_default();
38 let cid = match Cid::try_from(hash) {
39 Ok(cid) => cid,
40 Err(e) => return Err(InvalidCidInLink::from((nth, link, e))),
41 };
42 let name = match link.Name {
43 Some(Cow::Borrowed(s)) if !s.is_empty() => s.to_owned(),
44 None | Some(Cow::Borrowed(_)) => todo!("link cannot be empty"),
45 Some(Cow::Owned(_s)) => unreachable!("FlatUnixFs is never transformed to owned"),
46 };
47 assert!(!name.contains('/'));
48 Ok((cid, name, nested_depth))
49}
50
51fn convert_sharded_link(
53 nested_depth: usize,
54 sibling_depth: usize,
55 nth: usize,
56 link: PBLink<'_>,
57) -> Result<(Cid, String, usize), InvalidCidInLink> {
58 let hash = link.Hash.as_deref().unwrap_or_default();
59 let cid = match Cid::try_from(hash) {
60 Ok(cid) => cid,
61 Err(e) => return Err(InvalidCidInLink::from((nth, link, e))),
62 };
63 let (depth, name) = match link.Name {
64 Some(Cow::Borrowed(s)) if s.len() > 2 => (nested_depth, s[2..].to_owned()),
65 Some(Cow::Borrowed(s)) if s.len() == 2 => (sibling_depth, String::from("")),
66 None | Some(Cow::Borrowed(_)) => todo!("link cannot be empty"),
67 Some(Cow::Owned(_s)) => unreachable!("FlatUnixFs is never transformed to owned"),
68 };
69 assert!(!name.contains('/'));
70 Ok((cid, name, depth))
71}
72
73impl Walker {
74 pub fn new(cid: Cid, root_name: String) -> Walker {
76 let depth = if root_name.is_empty() { 1 } else { 2 };
78 let next = Some((cid, root_name, depth));
79
80 Walker {
81 current: None,
82 next,
83 pending: Vec::new(),
84 should_continue: true,
85 }
86 }
87
88 pub fn pending_links<'a>(&'a self) -> (&'a Cid, impl Iterator<Item = &'a Cid> + 'a) {
90 use InnerKind::*;
91 let cids = self.pending.iter().map(|(cid, ..)| cid).rev();
93
94 match self.current.as_ref().map(|c| &c.kind) {
95 Some(File(Some(ref visit), _)) => {
96 let (first, rest) = visit.pending_links();
97 let next = self.next.iter().map(|(cid, _, _)| cid);
98 (first, Either::Left(rest.chain(next.chain(cids))))
99 }
100 _ => {
101 let next = self
102 .next
103 .as_ref()
104 .expect("we've validated that we have the next in new and continue_walk");
105 (&next.0, Either::Right(cids))
106 }
107 }
108 }
109
110 pub fn next<'a: 'c, 'b: 'c, 'c>(
116 &'a mut self,
117 bytes: &'b [u8],
118 cache: &mut Option<Cache>,
119 ) -> Result<ContinuedWalk<'c>, Error> {
120 let Self {
121 current,
122 next,
123 pending,
124 should_continue,
125 } = self;
126
127 *should_continue = false;
128
129 if let Some(InnerEntry {
130 cid,
131 kind: InnerKind::File(visit @ Some(_), sz),
132 metadata,
133 path,
134 ..
135 }) = current
136 {
137 let (bytes, step) = visit.take().unwrap().continue_walk(bytes, cache)?;
139 let file_continues = step.is_some();
140 let segment = FileSegment::later(bytes, !file_continues);
141 *visit = step;
142 if file_continues || next.is_some() {
143 *should_continue = true;
144 }
145 return Ok(ContinuedWalk::File(segment, cid, path, metadata, *sz));
146 }
147
148 let flat = FlatUnixFs::try_from(bytes)?;
149 let metadata = Metadata::from(&flat.data);
150
151 match flat.data.Type {
152 UnixFsType::Directory => {
153 let flat = crate::dir::check_directory_supported(flat)?;
154 let (cid, name, depth) = next.take().expect("validated at new and earlier");
155
156 let links = flat
159 .links
160 .into_iter()
161 .enumerate()
162 .map(|(nth, link)| convert_link(depth + 1, nth, link))
163 .rev();
164
165 for link in links {
168 pending.push(link?);
169 }
170
171 if let next_local @ Some(_) = pending.pop() {
172 *next = next_local;
173 *should_continue = true;
174 }
175
176 Ok(match current {
177 None => {
178 *current = Some(InnerEntry::new_root_dir(cid, metadata, &name, depth));
179 let ie = current.as_ref().unwrap();
180 ContinuedWalk::RootDirectory(&ie.cid, &ie.path, &ie.metadata)
181 }
182 Some(ie) => {
183 ie.as_directory(cid, &name, depth, metadata);
184 ContinuedWalk::Directory(&ie.cid, &ie.path, &ie.metadata)
185 }
186 })
187 }
188 UnixFsType::HAMTShard => {
189 let flat = crate::dir::check_hamtshard_supported(flat)?;
190 let (cid, name, depth) = next.take().expect("validated at start and this method");
191
192 let links = flat
195 .links
196 .into_iter()
197 .enumerate()
198 .map(|(nth, link)| convert_sharded_link(depth + 1, depth, nth, link))
199 .rev();
200
201 for link in links {
205 pending.push(link?);
206 }
207
208 if let next_local @ Some(_) = pending.pop() {
209 *next = next_local;
210 *should_continue = true;
211 }
212
213 Ok(match current {
214 None => {
215 *current = Some(InnerEntry::new_root_bucket(cid, metadata, &name, depth));
216 let ie = current.as_ref().unwrap();
217 ContinuedWalk::RootDirectory(&ie.cid, &ie.path, &ie.metadata)
218 }
219 Some(ie) => {
220 if name.is_empty() {
222 ie.as_bucket(cid, &name, depth);
223 ContinuedWalk::Bucket(&ie.cid, &ie.path)
224 }
225 else {
227 ie.as_bucket_root(cid, &name, depth, metadata);
228 ContinuedWalk::RootDirectory(&ie.cid, &ie.path, &ie.metadata)
229 }
230 }
231 })
232 }
233 UnixFsType::Raw | UnixFsType::File => {
234 let (bytes, file_size, metadata, step) =
235 IdleFileVisit::default().start_from_parsed(flat, cache)?;
236 let (cid, name, depth) = next.take().expect("validated at new and earlier");
237 let file_continues = step.is_some();
238
239 match current {
240 None => {
241 let ie =
242 InnerEntry::new_root_file(cid, metadata, &name, step, file_size, depth);
243 *current = Some(ie);
244 }
245 Some(ie) => {
246 ie.as_file(cid, &name, depth, metadata, step, file_size);
247 }
248 };
249
250 let next_local = pending.pop();
251 if file_continues || next_local.is_some() {
252 *next = next_local;
253 *should_continue = true;
254 }
255
256 let segment = FileSegment::first(bytes, !file_continues);
257
258 let ie = current.as_ref().unwrap();
259 Ok(ContinuedWalk::File(
260 segment,
261 &ie.cid,
262 &ie.path,
263 &ie.metadata,
264 file_size,
265 ))
266 }
267 UnixFsType::Metadata => Err(Error::UnsupportedType(flat.data.Type.into())),
268 UnixFsType::Symlink => {
269 let contents = match flat.data.Data {
270 Some(Cow::Borrowed(bytes)) if !bytes.is_empty() => bytes,
271 None | Some(Cow::Borrowed(_)) => &[][..],
272 _ => unreachable!("never used into_owned"),
273 };
274
275 let (cid, name, depth) = next.take().expect("continued without next");
276 match current {
277 None => {
278 let ie = InnerEntry::new_root_symlink(cid, metadata, &name, depth);
279 *current = Some(ie);
280 }
281 Some(ie) => {
282 ie.as_symlink(cid, &name, depth, metadata);
283 }
284 };
285
286 if let next_local @ Some(_) = pending.pop() {
287 *next = next_local;
288 *should_continue = true;
289 }
290
291 let ie = current.as_ref().unwrap();
292 Ok(ContinuedWalk::Symlink(
293 contents,
294 &ie.cid,
295 &ie.path,
296 &ie.metadata,
297 ))
298 }
299 }
300 }
301
302 pub fn should_continue(&self) -> bool {
304 self.should_continue
305 }
306
307 }
312
313struct InnerEntry {
315 cid: Cid,
316 kind: InnerKind,
317 path: PathBuf,
318 metadata: Metadata,
319 depth: usize,
320}
321
322impl From<InnerEntry> for Metadata {
323 fn from(e: InnerEntry) -> Self {
324 e.metadata
325 }
326}
327
328#[derive(Debug)]
329enum InnerKind {
330 RootDirectory,
332 BucketAtRoot,
334 RootBucket,
336 Bucket,
338 Directory,
340 File(Option<FileVisit>, u64),
342 Symlink,
344}
345
346impl InnerEntry {
347 fn new_root_dir(cid: Cid, metadata: Metadata, name: &str, depth: usize) -> Self {
348 let mut path = PathBuf::new();
349 path.push(name);
350 Self {
351 cid,
352 kind: InnerKind::RootDirectory,
353 path,
354 metadata,
355 depth,
356 }
357 }
358
359 fn new_root_bucket(cid: Cid, metadata: Metadata, name: &str, depth: usize) -> Self {
360 let mut path = PathBuf::new();
361 path.push(name);
362 Self {
363 cid,
364 kind: InnerKind::BucketAtRoot,
365 path,
366 metadata,
367 depth,
368 }
369 }
370
371 fn new_root_file(
372 cid: Cid,
373 metadata: Metadata,
374 name: &str,
375 step: Option<FileVisit>,
376 file_size: u64,
377 depth: usize,
378 ) -> Self {
379 let mut path = PathBuf::new();
380 path.push(name);
381 Self {
382 cid,
383 kind: InnerKind::File(step, file_size),
384 path,
385 metadata,
386 depth,
387 }
388 }
389
390 fn new_root_symlink(cid: Cid, metadata: Metadata, name: &str, depth: usize) -> Self {
391 let mut path = PathBuf::new();
392 path.push(name);
393 Self {
394 cid,
395 kind: InnerKind::Symlink,
396 path,
397 metadata,
398 depth,
399 }
400 }
401
402 fn set_path(&mut self, name: &str, depth: usize) {
403 debug_assert_eq!(self.depth, self.path.ancestors().count());
404
405 while self.depth >= depth && self.depth > 0 {
406 assert!(self.path.pop());
407 self.depth = self
408 .depth
409 .checked_sub(1)
410 .expect("undeflowed path components");
411 }
412
413 self.path.push(name);
414 self.depth = depth;
415
416 debug_assert_eq!(self.depth, self.path.ancestors().count());
417 }
418
419 fn as_directory(&mut self, cid: Cid, name: &str, depth: usize, metadata: Metadata) {
420 use InnerKind::*;
421 match self.kind {
422 RootDirectory
423 | BucketAtRoot
424 | Bucket
425 | RootBucket
426 | Directory
427 | File(None, _)
428 | Symlink => {
429 self.cid = cid;
430 self.kind = Directory;
431 self.set_path(name, depth);
432 self.metadata = metadata;
433 }
434 ref x => unreachable!("directory ({}, {}, {}) following {:?}", cid, name, depth, x),
435 }
436 }
437
438 fn as_bucket_root(&mut self, cid: Cid, name: &str, depth: usize, metadata: Metadata) {
439 use InnerKind::*;
440 match self.kind {
441 RootDirectory
442 | BucketAtRoot
443 | Bucket
444 | RootBucket
445 | Directory
446 | File(None, _)
447 | Symlink => {
448 self.cid = cid;
449 self.kind = RootBucket;
450 self.set_path(name, depth);
451 self.metadata = metadata;
452 }
453 ref x => unreachable!(
454 "root bucket ({}, {}, {}) following {:?}",
455 cid, name, depth, x
456 ),
457 }
458 }
459
460 fn as_bucket(&mut self, cid: Cid, name: &str, depth: usize) {
461 use InnerKind::*;
462 match self.kind {
463 BucketAtRoot => {
464 assert_eq!(self.depth, depth, "{:?}", self.path);
465 }
466 RootBucket | Bucket | File(None, _) | Symlink => {
467 self.cid = cid;
468 self.kind = Bucket;
469
470 assert!(name.is_empty());
471 while self.depth > depth {
473 assert!(self.path.pop());
474 self.depth = self
475 .depth
476 .checked_sub(1)
477 .expect("underlowed depth calculation during bucket->bucket");
478 }
479
480 assert_eq!(self.depth, depth, "{:?}", self.path);
481 }
482 ref x => unreachable!("bucket ({}, {}, {}) following {:?}", cid, name, depth, x),
483 }
484 }
485
486 fn as_file(
487 &mut self,
488 cid: Cid,
489 name: &str,
490 depth: usize,
491 metadata: Metadata,
492 step: Option<FileVisit>,
493 file_size: u64,
494 ) {
495 use InnerKind::*;
496 match self.kind {
497 RootDirectory
498 | BucketAtRoot
499 | RootBucket
500 | Bucket
501 | Directory
502 | File(None, _)
503 | Symlink => {
504 self.cid = cid;
505 self.kind = File(step, file_size);
506 self.set_path(name, depth);
507 self.metadata = metadata;
508 }
509 ref x => unreachable!(
510 "file ({}, {}, {}, {}) following {:?}",
511 cid, name, depth, file_size, x
512 ),
513 }
514 }
515
516 fn as_symlink(&mut self, cid: Cid, name: &str, depth: usize, metadata: Metadata) {
517 use InnerKind::*;
518 match self.kind {
519 Bucket
520 | BucketAtRoot
521 | Directory
522 | File(None, _)
523 | RootBucket
524 | RootDirectory
525 | Symlink => {
526 self.cid = cid;
527 self.kind = Symlink;
528 self.set_path(name, depth);
529 self.metadata = metadata;
530 }
531 ref x => unreachable!("symlink ({}, {}, {}) following {:?}", cid, name, depth, x),
532 }
533 }
534}
535
536impl fmt::Debug for InnerEntry {
537 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
538 fmt.debug_struct("InnerEntry")
539 .field("depth", &self.depth)
540 .field("kind", &self.kind)
541 .field("cid", &format_args!("{}", self.cid))
542 .field("path", &self.path)
543 .field("metadata", &self.metadata)
544 .finish()
545 }
546}
547
548#[derive(Debug)]
550pub enum ContinuedWalk<'a> {
551 Bucket(&'a Cid, &'a Path),
553 Directory(&'a Cid, &'a Path, &'a Metadata),
555 File(FileSegment<'a>, &'a Cid, &'a Path, &'a Metadata, u64),
558 RootDirectory(&'a Cid, &'a Path, &'a Metadata),
560 Symlink(&'a [u8], &'a Cid, &'a Path, &'a Metadata),
563}
564
565impl ContinuedWalk<'_> {
566 #[cfg(test)]
567 fn path(&self) -> &Path {
568 match self {
569 Self::Bucket(_, p)
570 | Self::Directory(_, p, ..)
571 | Self::File(_, _, p, ..)
572 | Self::RootDirectory(_, p, ..)
573 | Self::Symlink(_, _, p, ..) => p,
574 }
575 }
576}
577
578#[derive(Debug)]
581pub struct FileSegment<'a> {
582 bytes: &'a [u8],
583 first_block: bool,
584 last_block: bool,
585}
586
587impl<'a> FileSegment<'a> {
588 fn first(bytes: &'a [u8], last_block: bool) -> Self {
589 FileSegment {
590 bytes,
591 first_block: true,
592 last_block,
593 }
594 }
595
596 fn later(bytes: &'a [u8], last_block: bool) -> Self {
597 FileSegment {
598 bytes,
599 first_block: false,
600 last_block,
601 }
602 }
603
604 pub fn is_first(&self) -> bool {
608 self.first_block
609 }
610
611 pub fn is_last(&self) -> bool {
615 self.last_block
616 }
617
618 pub fn as_bytes(&self) -> &'a [u8] {
621 self.bytes
622 }
623}
624
625impl AsRef<[u8]> for FileSegment<'_> {
626 fn as_ref(&self) -> &[u8] {
627 &self.bytes
628 }
629}
630
631#[derive(Debug)]
633pub enum Error {
634 UnsupportedType(UnexpectedNodeType),
638
639 UnexpectedType(UnexpectedNodeType),
641
642 DagPbParsingFailed(quick_protobuf::Error),
644
645 UnixFsParsingFailed(quick_protobuf::Error),
647
648 EmptyDagPbNode,
650
651 InvalidCid(InvalidCidInLink),
653
654 File(FileError),
656
657 UnsupportedDirectory(UnexpectedDirectoryProperties),
659
660 UnsupportedHAMTShard(ShardError),
662}
663
664impl From<ParsingFailed<'_>> for Error {
665 fn from(e: ParsingFailed<'_>) -> Self {
666 use ParsingFailed::*;
667 match e {
668 InvalidDagPb(e) => Error::DagPbParsingFailed(e),
669 InvalidUnixFs(e, _) => Error::UnixFsParsingFailed(e),
670 NoData(_) => Error::EmptyDagPbNode,
671 }
672 }
673}
674
675impl From<InvalidCidInLink> for Error {
676 fn from(e: InvalidCidInLink) -> Self {
677 Error::InvalidCid(e)
678 }
679}
680
681impl From<FileReadFailed> for Error {
682 fn from(e: FileReadFailed) -> Self {
683 use FileReadFailed::*;
684 match e {
685 File(e) => Error::File(e),
686 UnexpectedType(ut) => Error::UnexpectedType(ut),
687 Read(_) => unreachable!("FileVisit does not parse any blocks"),
688 InvalidCid(l) => Error::InvalidCid(l),
689 }
690 }
691}
692
693impl From<UnexpectedDirectoryProperties> for Error {
694 fn from(e: UnexpectedDirectoryProperties) -> Self {
695 Error::UnsupportedDirectory(e)
696 }
697}
698
699impl From<ShardError> for Error {
700 fn from(e: ShardError) -> Self {
701 Error::UnsupportedHAMTShard(e)
702 }
703}
704
705impl fmt::Display for Error {
706 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
707 use Error::*;
708
709 match self {
710 UnsupportedType(ut) => write!(fmt, "unsupported UnixFs type: {:?}", ut),
711 UnexpectedType(ut) => write!(fmt, "link to unexpected UnixFs type from File: {:?}", ut),
712 DagPbParsingFailed(e) => write!(fmt, "failed to parse the outer dag-pb: {}", e),
713 UnixFsParsingFailed(e) => write!(fmt, "failed to parse the inner UnixFs: {}", e),
714 EmptyDagPbNode => write!(fmt, "failed to parse the inner UnixFs: no data"),
715 InvalidCid(e) => write!(fmt, "link contained an invalid Cid: {}", e),
716 File(e) => write!(fmt, "invalid file: {}", e),
717 UnsupportedDirectory(udp) => write!(fmt, "unsupported directory: {}", udp),
718 UnsupportedHAMTShard(se) => write!(fmt, "unsupported hamtshard: {}", se),
719 }
720 }
721}
722
723impl std::error::Error for Error {}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728 use crate::test_support::FakeBlockstore;
729 use std::collections::HashMap;
730 use std::path::PathBuf;
731
732 #[test]
733 fn walk_two_file_directory_empty() {
734 two_file_directory_scenario("");
735 }
736
737 #[test]
738 fn walk_two_file_directory_named() {
739 two_file_directory_scenario("foo");
740 }
741
742 fn two_file_directory_scenario(root_name: &str) {
743 println!("new two_file_directory_scenario");
744 let mut counts =
745 walk_everything(root_name, "QmPTotyhVnnfCu9R4qwR4cdhpi5ENaiP8ZJfdqsm8Dw2jB");
746
747 let mut pb = PathBuf::new();
748 pb.push(root_name);
749 counts.checked_removal(&pb, 1);
750
751 pb.push("QmVkvLsSEm2uJx1h5Fqukje8mMPYg393o5C2kMCkF2bBTA");
752 counts.checked_removal(&pb, 1);
753
754 pb.push("foobar.balanced");
755 counts.checked_removal(&pb, 5);
756
757 assert!(pb.pop());
758 pb.push("foobar.trickle");
759 counts.checked_removal(&pb, 5);
760
761 assert!(counts.is_empty(), "{:#?}", counts);
762 }
763
764 #[test]
765 fn sharded_dir_different_root_empty() {
766 sharded_dir_scenario("");
767 }
768
769 #[test]
770 fn sharded_dir_different_root_named() {
771 sharded_dir_scenario("foo");
772 }
773
774 fn sharded_dir_scenario(root_name: &str) {
775 use core::fmt::Write;
776
777 let mut counts =
782 walk_everything(root_name, "QmZbFPTnDBMWbQ6iBxQAhuhLz8Nu9XptYS96e7cuf5wvbk");
783 let mut buf = PathBuf::from(root_name);
784
785 counts.checked_removal(&buf, 9);
786
787 let indices = [38, 48, 50, 58, 9, 33, 4, 34, 17, 37, 40, 16, 41, 3, 25, 49];
788 let mut fmtbuf = String::new();
789
790 for (index, i) in indices.iter().enumerate() {
791 fmtbuf.clear();
792 write!(fmtbuf, "long-named-file-{:03}", i).unwrap();
793
794 if index > 0 {
795 buf.pop();
796 }
797 buf.push(&fmtbuf);
798
799 counts.checked_removal(&buf, 1);
800 }
801
802 assert!(counts.is_empty(), "{:#?}", counts);
803 }
804
805 #[test]
806 fn top_level_single_block_file_empty() {
807 single_block_top_level_file_scenario("");
808 }
809
810 #[test]
811 fn top_level_single_block_file_named() {
812 single_block_top_level_file_scenario("empty.txt");
813 }
814
815 fn single_block_top_level_file_scenario(root_name: &str) {
816 let mut counts =
817 walk_everything(root_name, "QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH");
818 let buf = PathBuf::from(root_name);
819 counts.checked_removal(&buf, 1);
820 }
821
822 #[test]
823 fn top_level_symlink_empty() {
824 top_level_symlink_scenario("");
825 }
826
827 #[test]
828 fn top_level_symlink_named() {
829 top_level_symlink_scenario("this_links_to_foobar");
830 }
831
832 fn top_level_symlink_scenario(root_name: &str) {
833 let mut counts =
834 walk_everything(root_name, "QmNgQEdXVdLw79nH2bnxLMxnyWMaXrijfqMTiDVat3iyuz");
835 let buf = PathBuf::from(root_name);
836 counts.checked_removal(&buf, 1);
837 }
838
839 #[test]
840 fn top_level_multiblock_file_empty() {
841 top_level_multiblock_file_scenario("");
842 }
843
844 #[test]
845 fn top_level_multiblock_file_named() {
846 top_level_multiblock_file_scenario("foobar_and_newline.txt");
847 }
848
849 fn top_level_multiblock_file_scenario(root_name: &str) {
850 let mut counts =
851 walk_everything(root_name, "QmWfQ48ChJUj4vWKFsUDe4646xCBmXgdmNfhjz9T7crywd");
852 let buf = PathBuf::from(root_name);
853 counts.checked_removal(&buf, 5);
854 }
855
856 #[test]
857 fn test_walked_file_segments() {
858 let blocks = FakeBlockstore::with_fixtures();
859
860 let trickle_foobar =
861 cid::Cid::try_from("QmWfQ48ChJUj4vWKFsUDe4646xCBmXgdmNfhjz9T7crywd").unwrap();
862 let mut walker = Walker::new(trickle_foobar, String::new());
863
864 let mut counter = 0;
865
866 while walker.should_continue() {
867 let (next, _) = walker.pending_links();
868
869 let block = blocks.get_by_cid(&next);
870
871 counter += 1;
872
873 match walker.next(&block, &mut None).unwrap() {
874 ContinuedWalk::File(segment, ..) => {
875 match counter {
876 1 => {
877 assert!(segment.as_ref().is_empty());
879 assert!(segment.is_first());
880 assert!(!segment.is_last());
881 }
882 2..=4 => {
883 assert_eq!(segment.as_ref().len(), 2);
884 assert!(!segment.is_first());
885 assert!(!segment.is_last());
886 }
887 5 => {
888 assert_eq!(segment.as_ref().len(), 1);
889 assert!(!segment.is_first());
890 assert!(segment.is_last());
891 }
892 _ => unreachable!(),
893 }
894 }
895 x => unreachable!("{:?}", x),
896 };
897 }
898 }
899
900 trait CountsExt {
901 fn checked_removal(&mut self, key: &PathBuf, expected: usize);
902 }
903
904 impl CountsExt for HashMap<PathBuf, usize> {
905 fn checked_removal(&mut self, key: &PathBuf, expected: usize) {
906 use std::collections::hash_map::Entry::*;
907
908 match self.entry(key.clone()) {
909 Occupied(oe) => {
910 assert_eq!(oe.remove(), expected);
911 }
912 Vacant(_) => {
913 panic!(
914 "no such key {:?} (expected {}) in {:#?}",
915 key, expected, self
916 );
917 }
918 }
919 }
920 }
921
922 fn walk_everything(root_name: &str, cid: &str) -> HashMap<PathBuf, usize> {
923 let mut ret = HashMap::new();
924
925 let blocks = FakeBlockstore::with_fixtures();
926
927 let mut cache = None;
928 let mut walker = Walker::new(cid::Cid::try_from(cid).unwrap(), root_name.to_string());
929
930 while walker.should_continue() {
931 let (next, _) = walker.pending_links();
932 let block = blocks.get_by_cid(next);
933 let cw = walker.next(block, &mut cache).unwrap();
934 *ret.entry(PathBuf::from(cw.path())).or_insert(0) += 1;
935 }
936
937 ret
938 }
939}