1use crate::path::{IpfsPath, PathRoot};
4use crate::repo::{DataStore, DefaultStorage, Repo};
5use crate::{Block, Error};
6use anyhow::anyhow;
7use bytes::Bytes;
8use futures::future::BoxFuture;
9use futures::stream::BoxStream;
10use futures::{FutureExt, Stream, StreamExt};
11use ipld_core::cid::{Cid, Version};
12use multihash_codetable::{Code, MultihashDigest};
13use rust_unixfs::dir::builder::{BufferingTreeBuilder, TreeOptions};
14use rust_unixfs::dir::{DirLink, NodeDescription, describe};
15use rust_unixfs::file::adder::{
16 FileAdder, FileBranchLink, build_file_from_leaves, parse_file_branch, rebuild_file_branch,
17};
18use rust_unixfs::file::visit::IdleFileVisit;
19use std::collections::{BTreeMap, BTreeSet, HashSet};
20use std::future::IntoFuture;
21#[cfg(not(target_arch = "wasm32"))]
22use std::path::Path;
23use std::pin::Pin;
24use std::str::FromStr;
25use std::task::{Context, Poll};
26#[cfg(not(target_arch = "wasm32"))]
27use tokio::io::AsyncWriteExt;
28
29const ROOT_KEY: &[u8] = b"/mfs/root";
31
32const VERSION: Version = Version::V1;
33const HASHER: Code = Code::Sha2_256;
34const RAW_LEAF_CODEC: u64 = 0x55;
35const CHUNK: u64 = 256 * 1024;
36const MAX_FILE_SIZE: u64 = 8 << 30;
37
38type DirMap = BTreeMap<String, DirEntry>;
40
41#[derive(Debug, Clone, Copy)]
42struct DirEntry {
43 cid: Cid,
44 tsize: u64,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum MfsKind {
50 File { size: u64 },
51 Directory,
52 Symlink,
53}
54
55#[derive(Debug, Clone)]
57pub struct MfsEntry {
58 pub name: String,
59 pub cid: Cid,
60 pub kind: MfsKind,
61 pub size: u64,
63}
64
65#[derive(Debug, Clone)]
67pub struct MfsStat {
68 pub cid: Cid,
69 pub kind: MfsKind,
70 pub size: u64,
72 pub cumulative_size: u64,
74 pub blocks: usize,
76}
77
78#[derive(Debug, thiserror::Error)]
79pub enum MfsError {
80 #[error("'{0}' does not exist")]
81 NotFound(String),
82 #[error("'{0}' is a directory")]
83 IsDirectory(String),
84 #[error("'{0}' already exists")]
85 AlreadyExists(String),
86 #[error("'{0}' is a non-empty directory; pass recursive")]
87 NotEmpty(String),
88 #[error("'{0}' is a symlink")]
89 IsSymlink(String),
90 #[error("the write would exceed the MFS file size limit")]
91 FileSizeLimit,
92 #[error("the root directory cannot be modified")]
93 RootImmutable,
94}
95
96#[derive(Debug, Clone, Default)]
97pub struct WriteOptions {
98 pub offset: u64,
99 pub create: bool,
100 pub parents: bool,
101 pub truncate: bool,
102}
103
104#[derive(Debug)]
105pub enum WriteStatus {
106 Progress { written: u64, total: Option<u64> },
107 Completed { cid: Cid },
108 Failed { error: Error },
109}
110
111pub struct MfsWrite {
112 inner: BoxStream<'static, WriteStatus>,
113}
114
115impl Stream for MfsWrite {
116 type Item = WriteStatus;
117
118 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
119 self.inner.poll_next_unpin(cx)
120 }
121}
122
123impl IntoFuture for MfsWrite {
124 type Output = Result<Cid, Error>;
125 type IntoFuture = BoxFuture<'static, Result<Cid, Error>>;
126
127 fn into_future(self) -> Self::IntoFuture {
128 async move {
129 let mut stream = self.inner;
130 let mut cid = None;
131 while let Some(status) = stream.next().await {
132 match status {
133 WriteStatus::Completed { cid: completed } => cid = Some(completed),
134 WriteStatus::Failed { error } => return Err(error),
135 WriteStatus::Progress { .. } => {}
136 }
137 }
138 cid.ok_or_else(|| anyhow!("streaming write produced no result"))
139 }
140 .boxed()
141 }
142}
143
144#[derive(Debug)]
145pub enum ReadStatus {
146 Progress { written: u64, total: u64 },
147 Completed { written: u64 },
148 Failed { error: Error },
149}
150
151pub struct MfsRead {
152 inner: BoxStream<'static, ReadStatus>,
153}
154
155impl Stream for MfsRead {
156 type Item = ReadStatus;
157
158 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
159 self.inner.poll_next_unpin(cx)
160 }
161}
162
163impl IntoFuture for MfsRead {
164 type Output = Result<u64, Error>;
165 type IntoFuture = BoxFuture<'static, Result<u64, Error>>;
166
167 fn into_future(self) -> Self::IntoFuture {
168 async move {
169 let mut stream = self.inner;
170 let mut written = 0;
171 while let Some(status) = stream.next().await {
172 match status {
173 ReadStatus::Completed { written: total } => written = total,
174 ReadStatus::Failed { error } => return Err(error),
175 ReadStatus::Progress { .. } => {}
176 }
177 }
178 Ok(written)
179 }
180 .boxed()
181 }
182}
183
184#[derive(Clone)]
186pub struct Mfs {
187 repo: Repo<DefaultStorage>,
188 shard_threshold: Option<u64>,
189}
190
191impl Mfs {
192 pub(crate) fn new(repo: Repo<DefaultStorage>) -> Self {
193 Self {
194 repo,
195 shard_threshold: Some(256 * 1024),
196 }
197 }
198
199 pub fn with_shard_threshold(mut self, threshold: Option<u64>) -> Self {
200 self.shard_threshold = threshold;
201 self
202 }
203
204 fn repo(&self) -> &Repo<DefaultStorage> {
205 &self.repo
206 }
207
208 pub async fn root(&self) -> Result<Option<Cid>, Error> {
210 let mut guard = self.repo().inner.mfs_root.lock().await;
211 self.cached_root(&mut guard).await
212 }
213
214 pub async fn mkdir(&self, path: &str, parents: bool) -> Result<(), Error> {
217 let (cid, tsize, blocks) = encode_dir(&DirMap::new(), self.shard_threshold)?;
218 self.set_entry(path, DirEntry { cid, tsize }, blocks, parents, false)
219 .await
220 }
221
222 pub async fn write(&self, path: &str, data: &[u8], parents: bool) -> Result<(), Error> {
224 self.write_with(
225 path,
226 data,
227 WriteOptions {
228 offset: 0,
229 create: true,
230 parents,
231 truncate: true,
232 },
233 )
234 .await
235 }
236
237 pub async fn write_with(
241 &self,
242 path: &str,
243 data: &[u8],
244 opts: WriteOptions,
245 ) -> Result<(), Error> {
246 let comps = split_path(path)?;
247 if comps.is_empty() {
248 return Err(MfsError::RootImmutable.into());
249 }
250
251 let new_size = opts.offset.saturating_add(data.len() as u64);
252 if new_size > MAX_FILE_SIZE {
253 return Err(MfsError::FileSizeLimit.into());
254 }
255
256 let mut guard = self.repo().inner.mfs_root.lock().await;
257
258 let existing = self.resolve_file_locked(&mut guard, &comps).await?;
259 if existing.is_none() && !opts.create {
260 return Err(MfsError::NotFound(path.to_string()).into());
261 }
262
263 if let Some((cid, tsize, filesize)) = existing
264 && !opts.truncate
265 && new_size <= filesize
266 {
267 let edited = {
268 let _gc = self.repo().gc_guard().await;
269 self.overwrite_subtree(cid, 0, data, opts.offset).await?
270 };
271 if let Some((new_cid, blocks)) = edited {
272 return self
273 .set_entry_locked(
274 &mut guard,
275 path,
276 DirEntry {
277 cid: new_cid,
278 tsize,
279 },
280 blocks,
281 opts.parents,
282 true,
283 )
284 .await;
285 }
286 }
287
288 if let Some((cid, _, filesize)) = existing
289 && !opts.truncate
290 && new_size > filesize
291 && let Some((new_cid, new_tsize, blocks)) =
292 self.grow_file(cid, filesize, opts.offset, data).await?
293 {
294 return self
295 .set_entry_locked(
296 &mut guard,
297 path,
298 DirEntry {
299 cid: new_cid,
300 tsize: new_tsize,
301 },
302 blocks,
303 opts.parents,
304 true,
305 )
306 .await;
307 }
308
309 let mut content = if opts.truncate || existing.is_none() {
310 Vec::new()
311 } else {
312 self.read_existing_file_locked(&mut guard, &comps)
313 .await?
314 .unwrap_or_default()
315 };
316 let offset = opts.offset as usize;
317 let end = offset + data.len();
318 if content.len() < end {
319 content.resize(end, 0);
320 }
321 content[offset..end].copy_from_slice(data);
322
323 let (cid, tsize, blocks) = encode_file(&content)?;
324 self.set_entry_locked(
325 &mut guard,
326 path,
327 DirEntry { cid, tsize },
328 blocks,
329 opts.parents,
330 true,
331 )
332 .await
333 }
334
335 pub async fn truncate(&self, path: &str, size: u64) -> Result<(), Error> {
337 let comps = split_path(path)?;
338 if comps.is_empty() {
339 return Err(MfsError::RootImmutable.into());
340 }
341 if size > MAX_FILE_SIZE {
342 return Err(MfsError::FileSizeLimit.into());
343 }
344
345 let mut guard = self.repo().inner.mfs_root.lock().await;
346 let (file_cid, _, filesize) = self
347 .resolve_file_locked(&mut guard, &comps)
348 .await?
349 .ok_or_else(|| MfsError::NotFound(path.to_string()))?;
350 if size == filesize {
351 return Ok(());
352 }
353
354 let boundary = (size.min(filesize) / CHUNK) * CHUNK;
355 let read_end = size.min(filesize);
356 let result = {
357 let _gc = self.repo().gc_guard().await;
358 let mut new_tail = if boundary < read_end {
359 self.read_file_range(&file_cid, boundary, read_end).await?
360 } else {
361 Vec::new()
362 };
363 new_tail.resize((size - boundary) as usize, 0);
364 self.rebuild_from_boundary(file_cid, filesize, boundary, new_tail)
365 .await?
366 };
367
368 let (cid, tsize, blocks) = match result {
369 Some(r) => r,
370 None => {
371 let mut content = self
372 .read_existing_file_locked(&mut guard, &comps)
373 .await?
374 .unwrap_or_default();
375 content.resize(size as usize, 0);
376 encode_file(&content)?
377 }
378 };
379 self.set_entry_locked(
380 &mut guard,
381 path,
382 DirEntry { cid, tsize },
383 blocks,
384 false,
385 true,
386 )
387 .await
388 }
389
390 #[cfg(not(target_arch = "wasm32"))]
391 pub fn write_from_file(&self, path: &str, file: impl AsRef<Path>, parents: bool) -> MfsWrite {
393 let mfs = self.clone();
394 let path = path.to_string();
395 let file = file.as_ref().to_path_buf();
396
397 let inner = async_stream::stream! {
398 let meta = match tokio::fs::metadata(&file).await {
399 Ok(meta) => meta,
400 Err(e) => {
401 yield WriteStatus::Failed { error: e.into() };
402 return;
403 }
404 };
405 if !meta.is_file() {
406 yield WriteStatus::Failed {
407 error: std::io::Error::new(
408 std::io::ErrorKind::InvalidInput,
409 format!("'{}' is not a valid file", file.display()),
410 )
411 .into(),
412 };
413 return;
414 }
415 if meta.len() > MAX_FILE_SIZE {
416 yield WriteStatus::Failed {
417 error: std::io::Error::new(
418 std::io::ErrorKind::FileTooLarge,
419 format!("'{}' is too large", file.display()),
420 )
421 .into(),
422 };
423 return;
424 }
425 let opened = match tokio::fs::File::open(&file).await {
426 Ok(opened) => opened,
427 Err(e) => {
428 yield WriteStatus::Failed { error: e.into() };
429 return;
430 }
431 };
432 let reader = tokio_util::io::ReaderStream::new(opened).boxed();
433 for await status in mfs.write_progress(path, reader, parents, Some(meta.len())) {
434 yield status;
435 }
436 };
437
438 MfsWrite {
439 inner: inner.boxed(),
440 }
441 }
442
443 pub fn write_stream<S>(&self, path: &str, stream: S, parents: bool) -> MfsWrite
446 where
447 S: Stream<Item = std::io::Result<Bytes>> + Send + 'static,
448 {
449 let inner = self
450 .clone()
451 .write_progress(path.to_string(), stream.boxed(), parents, None);
452 MfsWrite {
453 inner: inner.boxed(),
454 }
455 }
456
457 fn write_progress(
458 self,
459 path: String,
460 mut stream: BoxStream<'static, std::io::Result<Bytes>>,
461 parents: bool,
462 total: Option<u64>,
463 ) -> impl Stream<Item = WriteStatus> {
464 async_stream::stream! {
465 let mut adder = FileAdder::builder()
466 .with_cid_version(VERSION)
467 .with_hasher(HASHER)
468 .build();
469 let mut tsize = 0u64;
470 let mut written = 0u64;
471 let mut root = None;
472
473 while let Some(chunk) = stream.next().await {
474 let chunk = match chunk {
475 Ok(chunk) => chunk,
476 Err(e) => {
477 yield WriteStatus::Failed { error: e.into() };
478 return;
479 }
480 };
481 let mut offset = 0;
482 while offset < chunk.len() {
483 let (ready, consumed) = adder.push(&chunk[offset..]);
484 let mut batch = Vec::new();
485 for (cid, block) in ready {
486 tsize += block.len() as u64;
487 root = Some(cid);
488 match Block::new(cid, block) {
489 Ok(block) => batch.push(block),
490 Err(e) => {
491 yield WriteStatus::Failed { error: e.into() };
492 return;
493 }
494 }
495 }
496 if !batch.is_empty()
497 && let Err(e) = self.repo().put_blocks(batch).await
498 {
499 yield WriteStatus::Failed { error: e };
500 return;
501 }
502 offset += consumed;
503 written += consumed as u64;
504 if written > MAX_FILE_SIZE {
505 yield WriteStatus::Failed { error: MfsError::FileSizeLimit.into() };
506 return;
507 }
508 }
509 yield WriteStatus::Progress { written, total };
510 }
511
512 let mut batch = Vec::new();
513 for (cid, block) in adder.finish() {
514 tsize += block.len() as u64;
515 root = Some(cid);
516 match Block::new(cid, block) {
517 Ok(block) => batch.push(block),
518 Err(e) => {
519 yield WriteStatus::Failed { error: e.into() };
520 return;
521 }
522 }
523 }
524 if !batch.is_empty()
525 && let Err(e) = self.repo().put_blocks(batch).await
526 {
527 yield WriteStatus::Failed { error: e };
528 return;
529 }
530
531 let cid = match root {
532 Some(cid) => cid,
533 None => {
534 yield WriteStatus::Failed { error: anyhow!("file produced no blocks") };
535 return;
536 }
537 };
538
539 match self
540 .set_entry(&path, DirEntry { cid, tsize }, Vec::new(), parents, true)
541 .await
542 {
543 Ok(()) => yield WriteStatus::Completed { cid },
544 Err(e) => yield WriteStatus::Failed { error: e },
545 }
546 }
547 }
548
549 pub async fn read(&self, path: &str) -> Result<Vec<u8>, Error> {
551 self.read_range(path, 0, None).await
552 }
553
554 pub async fn read_range(
556 &self,
557 path: &str,
558 offset: u64,
559 count: Option<u64>,
560 ) -> Result<Vec<u8>, Error> {
561 let stream = self.read_stream_range(path, offset, count);
562 futures::pin_mut!(stream);
563 let mut out = Vec::new();
564 while let Some(chunk) = stream.next().await {
565 out.extend_from_slice(&chunk?);
566 }
567 Ok(out)
568 }
569
570 pub fn read_stream(&self, path: &str) -> impl Stream<Item = Result<Bytes, Error>> {
572 self.read_stream_range(path, 0, None)
573 }
574
575 pub fn read_stream_range(
578 &self,
579 path: &str,
580 offset: u64,
581 count: Option<u64>,
582 ) -> impl Stream<Item = Result<Bytes, Error>> {
583 let mfs = self.clone();
584 let path = path.to_string();
585 let end = count.map_or(u64::MAX, |c| offset.saturating_add(c));
586
587 async_stream::try_stream! {
588 let comps = split_path(&path)?;
589 if comps.is_empty() {
590 Err::<(), _>(anyhow!("cannot read the root directory"))?;
591 }
592 let root = mfs
593 .snapshot_root()
594 .await?
595 .ok_or_else(|| anyhow!("MFS is empty"))?;
596 let cid = {
597 let _gc = mfs.repo().gc_guard().await;
598 mfs.resolve_from(root, &comps).await?.0
599 };
600
601 let block = mfs.get_block(&cid).await?;
602 match describe(block.data()) {
603 NodeDescription::Directory { .. } | NodeDescription::HamtShard { .. } => {
604 Err::<(), _>(MfsError::IsDirectory(path.clone()))?;
605 }
606 NodeDescription::Symlink => {
607 Err::<(), _>(MfsError::IsSymlink(path.clone()))?;
608 }
609 NodeDescription::Other => {
610 let leaf = block.data();
611 let start = (offset as usize).min(leaf.len());
612 let stop = match count {
613 Some(c) => start.saturating_add(c as usize).min(leaf.len()),
614 None => leaf.len(),
615 };
616 if start < stop {
617 yield Bytes::copy_from_slice(&leaf[start..stop]);
618 }
619 }
620 _ => {
621 let mut cache = None;
622 let (content, _, _, mut step) = IdleFileVisit::default()
623 .with_target_range(offset..end)
624 .start(block.data())?;
625 if !content.is_empty() {
626 yield Bytes::copy_from_slice(content);
627 }
628 while let Some(visit) = step {
629 let next = *visit.pending_links().0;
630 let block = mfs.get_block(&next).await?;
631 let (content, next_step) = visit.continue_walk(block.data(), &mut cache)?;
632 if !content.is_empty() {
633 yield Bytes::copy_from_slice(content);
634 }
635 step = next_step;
636 }
637 }
638 }
639 }
640 }
641
642 #[cfg(not(target_arch = "wasm32"))]
646 pub fn read_to_file(&self, path: &str, dest: impl AsRef<Path>) -> MfsRead {
647 let mfs = self.clone();
648 let path = path.to_string();
649 let dest = dest.as_ref().to_path_buf();
650
651 let inner = async_stream::stream! {
652 let total = match mfs.stat(&path).await {
653 Ok(stat) => match stat.kind {
654 MfsKind::File { size } => size,
655 _ => {
656 yield ReadStatus::Failed { error: anyhow!("'{path}' is not a file") };
657 return;
658 }
659 },
660 Err(e) => {
661 yield ReadStatus::Failed { error: e };
662 return;
663 }
664 };
665
666 let mut file = match tokio::fs::File::create(&dest).await {
667 Ok(file) => file,
668 Err(e) => {
669 yield ReadStatus::Failed { error: e.into() };
670 return;
671 }
672 };
673
674 let mut written = 0u64;
675 let stream = mfs.read_stream(&path);
676 futures::pin_mut!(stream);
677 while let Some(chunk) = stream.next().await {
678 let chunk = match chunk {
679 Ok(chunk) => chunk,
680 Err(e) => {
681 yield ReadStatus::Failed { error: e };
682 return;
683 }
684 };
685 if let Err(e) = file.write_all(&chunk).await {
686 yield ReadStatus::Failed { error: e.into() };
687 return;
688 }
689 written += chunk.len() as u64;
690 yield ReadStatus::Progress { written, total };
691 }
692
693 if let Err(e) = file.flush().await {
694 yield ReadStatus::Failed { error: e.into() };
695 return;
696 }
697 yield ReadStatus::Completed { written };
698 };
699
700 MfsRead {
701 inner: inner.boxed(),
702 }
703 }
704
705 pub async fn rm(&self, path: &str, recursive: bool) -> Result<(), Error> {
707 self.rm_inner(path, recursive, false).await
708 }
709
710 pub async fn rm_force(&self, path: &str, recursive: bool) -> Result<(), Error> {
712 self.rm_inner(path, recursive, true).await
713 }
714
715 async fn rm_inner(&self, path: &str, recursive: bool, force: bool) -> Result<(), Error> {
716 let comps = split_path(path)?;
717 let Some((name, dirs)) = comps.split_last() else {
718 return Err(MfsError::RootImmutable.into());
719 };
720
721 let mut guard = self.repo().inner.mfs_root.lock().await;
722 let (mut frames, names) = match self.load_chain(&mut guard, dirs, false).await {
723 Ok(chain) => chain,
724 Err(e) if force && is_not_found(&e) => return Ok(()),
725 Err(e) => return Err(e),
726 };
727
728 let entry = match frames.last().expect("root frame").get(name).copied() {
729 Some(entry) => entry,
730 None if force => return Ok(()),
731 None => return Err(MfsError::NotFound(path.to_string()).into()),
732 };
733
734 if !recursive
735 && let Ok(map) = self.load_dir(&entry.cid).await
736 && !map.is_empty()
737 {
738 return Err(MfsError::NotEmpty(path.to_string()).into());
739 }
740
741 frames.last_mut().expect("root frame").remove(name);
742 self.reencode_and_commit(&mut guard, frames, names, Vec::new())
743 .await
744 }
745
746 pub async fn cp(&self, from: &str, to: &str, parents: bool) -> Result<(), Error> {
749 let (cid, tsize) = if is_ipfs_path(from) {
750 let path = IpfsPath::from_str(from)?;
751 let root = match path.root() {
752 PathRoot::Ipld(cid) => *cid,
753 _ => return Err(anyhow!("cp source must resolve to an /ipfs or /ipld cid")),
754 };
755 let sub: Vec<String> = path.iter().map(|s| s.to_string()).collect();
756 let source = self.resolve_ipfs(root, &sub).await?;
757 let tsize = self.fetch_and_measure(source).await?;
758 (source, tsize)
759 } else {
760 let from_comps = split_path(from)?;
761 self.resolve(&from_comps).await?
762 };
763
764 let (dest, _) = self.resolve_dest(to, from).await?;
765 self.set_entry(&dest, DirEntry { cid, tsize }, Vec::new(), parents, false)
766 .await
767 }
768
769 pub async fn mv(&self, from: &str, to: &str, parents: bool) -> Result<(), Error> {
772 let from_comps = split_path(from)?;
773 if from_comps.is_empty() {
774 return Err(MfsError::RootImmutable.into());
775 }
776 let (cid, tsize) = self.resolve(&from_comps).await?;
777 let (dest, into_dir) = self.resolve_dest(to, from).await?;
778 let dest_comps = split_path(&dest)?;
779 if dest_comps == from_comps {
780 return Ok(());
781 }
782 if dest_comps.len() > from_comps.len() && dest_comps[..from_comps.len()] == from_comps[..] {
783 return Err(anyhow!("cannot move '{from}' into its own subdirectory"));
784 }
785 self.set_entry(
787 &dest,
788 DirEntry { cid, tsize },
789 Vec::new(),
790 parents,
791 !into_dir,
792 )
793 .await?;
794 self.rm(from, true).await
795 }
796
797 pub async fn ls(&self, path: &str) -> Result<Vec<MfsEntry>, Error> {
800 let comps = split_path(path)?;
801 let Some(root) = self.snapshot_root().await? else {
802 if comps.is_empty() {
803 return Ok(Vec::new());
804 }
805 return Err(anyhow!("MFS is empty"));
806 };
807
808 let _gc = self.repo().gc_guard().await;
809 let (cid, tsize) = self.resolve_from(root, &comps).await?;
810 let block = self.get_block(&cid).await?;
811
812 let map = match describe(block.data()) {
813 NodeDescription::Directory { links } => links_to_map(links),
814 NodeDescription::HamtShard { links } => {
815 let mut map = DirMap::new();
816 self.collect_shard(links, &mut map).await?;
817 map
818 }
819 _ => {
820 let name = comps.last().cloned().unwrap_or_default();
821 let kind = self.classify(&cid, tsize).await?;
822 return Ok(vec![MfsEntry {
823 name,
824 cid,
825 kind,
826 size: entry_size(kind, tsize),
827 }]);
828 }
829 };
830
831 let mut out = Vec::with_capacity(map.len());
832 for (name, entry) in map {
833 let kind = self.classify(&entry.cid, entry.tsize).await?;
834 out.push(MfsEntry {
835 name,
836 cid: entry.cid,
837 kind,
838 size: entry_size(kind, entry.tsize),
839 });
840 }
841 Ok(out)
842 }
843
844 pub async fn stat(&self, path: &str) -> Result<MfsStat, Error> {
846 let comps = split_path(path)?;
847 let Some(root) = self.snapshot_root().await? else {
848 if comps.is_empty() {
849 let (cid, tsize, _) = encode_dir(&DirMap::new(), self.shard_threshold)?;
850 return Ok(MfsStat {
851 cid,
852 kind: MfsKind::Directory,
853 size: tsize,
854 cumulative_size: tsize,
855 blocks: 0,
856 });
857 }
858 return Err(anyhow!("MFS is empty"));
859 };
860
861 let _gc = self.repo().gc_guard().await;
862 let (cid, tsize) = self.resolve_from(root, &comps).await?;
863 let block = self.get_block(&cid).await?;
864 let (kind, blocks, link_tsize) = match describe(block.data()) {
865 NodeDescription::Directory { links } | NodeDescription::HamtShard { links } => {
866 let sum = links.iter().map(|l| l.tsize).sum();
867 (MfsKind::Directory, links.len(), sum)
868 }
869 NodeDescription::Symlink => (MfsKind::Symlink, 0, 0),
870 NodeDescription::File { size } => (
871 MfsKind::File { size },
872 parse_file_branch(block.data()).map_or(0, |b| b.links.len()),
873 0,
874 ),
875 NodeDescription::Other => (
876 MfsKind::File {
877 size: block.data().len() as u64,
878 },
879 0,
880 0,
881 ),
882 };
883
884 let cumulative_size = if comps.is_empty() {
887 block.data().len() as u64 + link_tsize
888 } else {
889 tsize
890 };
891 Ok(MfsStat {
892 cid,
893 kind,
894 size: entry_size(kind, cumulative_size),
895 cumulative_size,
896 blocks,
897 })
898 }
899
900 pub async fn exists(&self, path: &str) -> Result<bool, Error> {
902 let comps = split_path(path)?;
903 Ok(self.resolve_kind(&comps).await?.is_some())
904 }
905
906 async fn resolve_kind(&self, comps: &[String]) -> Result<Option<MfsKind>, Error> {
908 let Some(root) = self.snapshot_root().await? else {
909 return Ok(comps.is_empty().then_some(MfsKind::Directory));
910 };
911 let _gc = self.repo().gc_guard().await;
912 match self.resolve_from(root, comps).await {
913 Ok((cid, tsize)) => Ok(Some(self.classify(&cid, tsize).await?)),
914 Err(_) => Ok(None),
915 }
916 }
917
918 async fn resolve_dest(&self, to: &str, source: &str) -> Result<(String, bool), Error> {
921 let trailing = to.ends_with('/');
922 let mut comps = split_path(to)?;
923 let into_dir =
924 trailing || matches!(self.resolve_kind(&comps).await?, Some(MfsKind::Directory));
925 if into_dir {
926 let name = base_name(source).ok_or_else(|| anyhow!("cp/mv source has no name"))?;
927 comps.push(name.to_string());
928 }
929 Ok((format!("/{}", comps.join("/")), into_dir))
930 }
931
932 async fn cached_root(&self, guard: &mut (bool, Option<Cid>)) -> Result<Option<Cid>, Error> {
935 if !guard.0 {
936 guard.1 = match self.repo().data_store().get(ROOT_KEY).await? {
937 Some(bytes) => Some(Cid::try_from(bytes.as_slice())?),
938 None => None,
939 };
940 guard.0 = true;
941 }
942 Ok(guard.1)
943 }
944
945 async fn snapshot_root(&self) -> Result<Option<Cid>, Error> {
947 let mut guard = self.repo().inner.mfs_root.lock().await;
948 self.cached_root(&mut guard).await
949 }
950
951 async fn set_entry(
955 &self,
956 path: &str,
957 entry: DirEntry,
958 entry_blocks: Vec<Block>,
959 parents: bool,
960 overwrite: bool,
961 ) -> Result<(), Error> {
962 let mut guard = self.repo().inner.mfs_root.lock().await;
963 self.set_entry_locked(&mut guard, path, entry, entry_blocks, parents, overwrite)
964 .await
965 }
966
967 async fn set_entry_locked(
968 &self,
969 guard: &mut (bool, Option<Cid>),
970 path: &str,
971 entry: DirEntry,
972 entry_blocks: Vec<Block>,
973 parents: bool,
974 overwrite: bool,
975 ) -> Result<(), Error> {
976 let comps = split_path(path)?;
977 let Some((name, dirs)) = comps.split_last() else {
978 return Err(MfsError::RootImmutable.into());
979 };
980
981 let (mut frames, names) = self.load_chain(guard, dirs, parents).await?;
982
983 let parent = frames.last_mut().expect("root frame");
984 if !overwrite && parent.contains_key(name) {
985 return Err(MfsError::AlreadyExists(path.to_string()).into());
986 }
987 parent.insert(name.clone(), entry);
988
989 self.reencode_and_commit(guard, frames, names, entry_blocks)
990 .await
991 }
992
993 async fn load_chain(
997 &self,
998 guard: &mut (bool, Option<Cid>),
999 dirs: &[String],
1000 create: bool,
1001 ) -> Result<(Vec<DirMap>, Vec<String>), Error> {
1002 let root_map = match self.cached_root(guard).await? {
1003 Some(cid) => self.load_dir(&cid).await?,
1004 None => DirMap::new(),
1005 };
1006
1007 let mut frames = vec![root_map];
1008 let mut names = Vec::with_capacity(dirs.len());
1009
1010 for comp in dirs {
1011 let next = match frames.last().expect("non-empty").get(comp).copied() {
1012 Some(entry) => self.load_dir(&entry.cid).await?,
1013 None if create => DirMap::new(),
1014 None => return Err(MfsError::NotFound(comp.clone()).into()),
1015 };
1016 names.push(comp.clone());
1017 frames.push(next);
1018 }
1019
1020 Ok((frames, names))
1021 }
1022
1023 async fn reencode_and_commit(
1026 &self,
1027 guard: &mut (bool, Option<Cid>),
1028 mut frames: Vec<DirMap>,
1029 names: Vec<String>,
1030 mut blocks: Vec<Block>,
1031 ) -> Result<(), Error> {
1032 for i in (0..frames.len()).rev() {
1033 let (cid, tsize, mut blks) = encode_dir(&frames[i], self.shard_threshold)?;
1034 blocks.append(&mut blks);
1035 if i == 0 {
1036 return self.commit_root(guard, cid, blocks).await;
1037 }
1038 frames[i - 1].insert(names[i - 1].clone(), DirEntry { cid, tsize });
1039 }
1040 unreachable!("frames always contains the root at index 0")
1041 }
1042
1043 async fn commit_root(
1046 &self,
1047 guard: &mut (bool, Option<Cid>),
1048 new_root: Cid,
1049 blocks: Vec<Block>,
1050 ) -> Result<(), Error> {
1051 let old = guard.1;
1052
1053 self.repo().put_blocks(blocks).await?;
1054 self.repo().pin(new_root).recursive().await?;
1055 self.repo()
1056 .data_store()
1057 .put(ROOT_KEY, &new_root.to_bytes())
1058 .await?;
1059
1060 guard.1 = Some(new_root);
1061 guard.0 = true;
1062
1063 if let Some(old) = old
1064 && old != new_root
1065 {
1066 let _ = self.repo().remove_pin(old).recursive().await;
1068 }
1069 Ok(())
1070 }
1071
1072 async fn resolve(&self, comps: &[String]) -> Result<(Cid, u64), Error> {
1075 let root = self
1076 .snapshot_root()
1077 .await?
1078 .ok_or_else(|| anyhow!("MFS is empty"))?;
1079 let _gc = self.repo().gc_guard().await;
1080 self.resolve_from(root, comps).await
1081 }
1082
1083 async fn resolve_from(&self, root: Cid, comps: &[String]) -> Result<(Cid, u64), Error> {
1086 let mut cid = root;
1087 let mut tsize = 0;
1088 for comp in comps {
1089 let map = self.load_dir(&cid).await?;
1090 let entry = map
1091 .get(comp)
1092 .ok_or_else(|| anyhow!("path not found: {comp}"))?;
1093 cid = entry.cid;
1094 tsize = entry.tsize;
1095 }
1096 Ok((cid, tsize))
1097 }
1098
1099 async fn load_dir(&self, cid: &Cid) -> Result<DirMap, Error> {
1101 let block = self.get_block(cid).await?;
1102 match describe(block.data()) {
1103 NodeDescription::Directory { links } => Ok(links_to_map(links)),
1104 NodeDescription::HamtShard { links } => {
1105 let mut map = DirMap::new();
1106 self.collect_shard(links, &mut map).await?;
1107 Ok(map)
1108 }
1109 _ => Err(anyhow!("{cid} is not a directory")),
1110 }
1111 }
1112
1113 async fn resolve_ipfs(&self, root: Cid, sub: &[String]) -> Result<Cid, Error> {
1114 let mut cid = root;
1115 for seg in sub {
1116 cid = self
1117 .resolve_name(cid, seg)
1118 .await?
1119 .ok_or_else(|| anyhow!("path not found in source: {seg}"))?;
1120 }
1121 Ok(cid)
1122 }
1123
1124 async fn resolve_name(&self, dir_cid: Cid, name: &str) -> Result<Option<Cid>, Error> {
1125 use rust_unixfs::dir::{MaybeResolved, resolve};
1126
1127 let block = self.repo().get_block(dir_cid).await?;
1128 let mut cache = None;
1129 let mut step = resolve(block.data(), name, &mut cache)?;
1130 loop {
1131 match step {
1132 MaybeResolved::Found(cid) => return Ok(Some(cid)),
1133 MaybeResolved::NotFound => return Ok(None),
1134 MaybeResolved::NeedToLoadMore(lookup) => {
1135 let next = *lookup.pending_links().0;
1136 let block = self.repo().get_block(next).await?;
1137 step = lookup.continue_walk(block.data(), &mut cache)?;
1138 }
1139 }
1140 }
1141 }
1142
1143 async fn fetch_and_measure(&self, root: Cid) -> Result<u64, Error> {
1144 let mut total = 0u64;
1145 let mut seen = HashSet::new();
1146 let mut stack = vec![root];
1147 while let Some(cid) = stack.pop() {
1148 if !seen.insert(cid) {
1149 continue;
1150 }
1151 let block = self.repo().get_block(cid).await?;
1152 total += block.data().len() as u64;
1153 let mut refs = BTreeSet::new();
1154 let _ = block.references(&mut refs);
1155 stack.extend(refs);
1156 }
1157 Ok(total)
1158 }
1159
1160 fn collect_shard<'a>(
1161 &'a self,
1162 links: Vec<DirLink>,
1163 map: &'a mut DirMap,
1164 ) -> BoxFuture<'a, Result<(), Error>> {
1165 async move {
1166 for link in links {
1167 if is_shard_prefix(&link.name) {
1168 let block = self.get_block(&link.target).await?;
1169 match describe(block.data()) {
1170 NodeDescription::HamtShard { links } => {
1171 self.collect_shard(links, map).await?;
1172 }
1173 _ => return Err(anyhow!("malformed HAMT shard under {}", link.target)),
1174 }
1175 } else {
1176 let name = link.name.get(2..).unwrap_or_default().to_string();
1177 map.insert(
1178 name,
1179 DirEntry {
1180 cid: link.target,
1181 tsize: link.tsize,
1182 },
1183 );
1184 }
1185 }
1186 Ok(())
1187 }
1188 .boxed()
1189 }
1190
1191 async fn classify(&self, cid: &Cid, _link_tsize: u64) -> Result<MfsKind, Error> {
1192 let block = self.get_block(cid).await?;
1193 Ok(match describe(block.data()) {
1194 NodeDescription::Directory { .. } | NodeDescription::HamtShard { .. } => {
1195 MfsKind::Directory
1196 }
1197 NodeDescription::File { size } => MfsKind::File { size },
1198 NodeDescription::Symlink => MfsKind::Symlink,
1199 NodeDescription::Other => MfsKind::File {
1200 size: block.data().len() as u64,
1201 },
1202 })
1203 }
1204
1205 async fn resolve_file_locked(
1206 &self,
1207 guard: &mut (bool, Option<Cid>),
1208 comps: &[String],
1209 ) -> Result<Option<(Cid, u64, u64)>, Error> {
1210 let Some(root) = self.cached_root(guard).await? else {
1211 return Ok(None);
1212 };
1213 let _gc = self.repo().gc_guard().await;
1214 let (cid, tsize) = match self.resolve_from(root, comps).await {
1215 Ok(resolved) => resolved,
1216 Err(_) => return Ok(None),
1217 };
1218 match self.classify(&cid, tsize).await? {
1219 MfsKind::File { size } => Ok(Some((cid, tsize, size))),
1220 MfsKind::Symlink => Err(MfsError::IsSymlink(format!("/{}", comps.join("/"))).into()),
1221 MfsKind::Directory => {
1222 Err(MfsError::IsDirectory(format!("/{}", comps.join("/"))).into())
1223 }
1224 }
1225 }
1226
1227 #[allow(clippy::type_complexity)]
1228 fn overwrite_subtree<'a>(
1229 &'a self,
1230 cid: Cid,
1231 node_start: u64,
1232 data: &'a [u8],
1233 data_start: u64,
1234 ) -> BoxFuture<'a, Result<Option<(Cid, Vec<Block>)>, Error>> {
1235 async move {
1236 let block = self.get_block(&cid).await?;
1237
1238 if let Some(branch) = parse_file_branch(block.data()) {
1239 let mut blocks = Vec::new();
1240 let mut links: Vec<FileBranchLink> = branch.links.clone();
1241 let write_end = data_start + data.len() as u64;
1242 let mut child_start = node_start;
1243 for (i, link) in branch.links.iter().enumerate() {
1244 let child_end = child_start + link.blocksize;
1245 if child_end > data_start && child_start < write_end {
1246 match self
1247 .overwrite_subtree(link.cid, child_start, data, data_start)
1248 .await?
1249 {
1250 Some((new_cid, mut child_blocks)) => {
1251 links[i].cid = new_cid;
1252 blocks.append(&mut child_blocks);
1253 }
1254 None => return Ok(None),
1255 }
1256 }
1257 child_start = child_end;
1258 }
1259 let (new_cid, bytes) =
1260 rebuild_file_branch(branch.filesize, &links, VERSION, HASHER);
1261 blocks.push(Block::new(new_cid, bytes)?);
1262 return Ok(Some((new_cid, blocks)));
1263 }
1264
1265 if !matches!(describe(block.data()), NodeDescription::Other) {
1266 return Ok(None);
1267 }
1268
1269 let leaf = block.data();
1270 let ov_start = node_start.max(data_start);
1271 let ov_end = (node_start + leaf.len() as u64).min(data_start + data.len() as u64);
1272 if ov_start >= ov_end {
1273 return Ok(Some((cid, Vec::new())));
1274 }
1275 let mut new_leaf = leaf.to_vec();
1276 let dst = (ov_start - node_start) as usize..(ov_end - node_start) as usize;
1277 let src = (ov_start - data_start) as usize..(ov_end - data_start) as usize;
1278 new_leaf[dst].copy_from_slice(&data[src]);
1279 let new_cid = Cid::new_v1(RAW_LEAF_CODEC, HASHER.digest(&new_leaf));
1280 Ok(Some((new_cid, vec![Block::new(new_cid, new_leaf)?])))
1281 }
1282 .boxed()
1283 }
1284
1285 async fn read_file_range(&self, cid: &Cid, start: u64, end: u64) -> Result<Vec<u8>, Error> {
1286 let block = self.get_block(cid).await?;
1287 if matches!(describe(block.data()), NodeDescription::Other) {
1288 let leaf = block.data();
1289 let s = (start as usize).min(leaf.len());
1290 let e = (end as usize).min(leaf.len());
1291 return Ok(leaf[s..e].to_vec());
1292 }
1293
1294 let mut out = Vec::new();
1295 let mut cache = None;
1296 let (content, _, _, mut step) = IdleFileVisit::default()
1297 .with_target_range(start..end)
1298 .start(block.data())?;
1299 out.extend_from_slice(content);
1300 while let Some(visit) = step {
1301 let next = *visit.pending_links().0;
1302 let block = self.get_block(&next).await?;
1303 let (content, next_step) = visit.continue_walk(block.data(), &mut cache)?;
1304 out.extend_from_slice(content);
1305 step = next_step;
1306 }
1307 Ok(out)
1308 }
1309
1310 fn collect_prefix_leaves<'a>(
1311 &'a self,
1312 cid: Cid,
1313 node_start: u64,
1314 boundary: u64,
1315 out: &'a mut Vec<FileBranchLink>,
1316 ) -> BoxFuture<'a, Result<bool, Error>> {
1317 async move {
1318 let block = self.get_block(&cid).await?;
1319 let Some(branch) = parse_file_branch(block.data()) else {
1320 return Ok(false);
1321 };
1322 let mut child_start = node_start;
1323 for link in &branch.links {
1324 if child_start >= boundary {
1325 break;
1326 }
1327 let child_end = child_start + link.blocksize;
1328 if link.cid.codec() == RAW_LEAF_CODEC {
1329 if child_end <= boundary && link.blocksize == CHUNK {
1330 out.push(link.clone());
1331 } else {
1332 return Ok(false);
1333 }
1334 } else if !self
1335 .collect_prefix_leaves(link.cid, child_start, boundary, out)
1336 .await?
1337 {
1338 return Ok(false);
1339 }
1340 child_start = child_end;
1341 }
1342 Ok(true)
1343 }
1344 .boxed()
1345 }
1346
1347 async fn prefix_leaves(
1348 &self,
1349 file_cid: Cid,
1350 filesize: u64,
1351 boundary: u64,
1352 ) -> Result<Option<Vec<FileBranchLink>>, Error> {
1353 if file_cid.codec() == RAW_LEAF_CODEC {
1354 let leaf = FileBranchLink {
1355 cid: file_cid,
1356 blocksize: filesize,
1357 tsize: filesize,
1358 };
1359 return Ok(Some(if filesize > 0 && filesize <= boundary {
1360 vec![leaf]
1361 } else {
1362 vec![]
1363 }));
1364 }
1365 let mut out = Vec::new();
1366 if self
1367 .collect_prefix_leaves(file_cid, 0, boundary, &mut out)
1368 .await?
1369 {
1370 Ok(Some(out))
1371 } else {
1372 Ok(None)
1373 }
1374 }
1375
1376 async fn rebuild_from_boundary(
1377 &self,
1378 file_cid: Cid,
1379 filesize: u64,
1380 boundary: u64,
1381 new_tail: Vec<u8>,
1382 ) -> Result<Option<(Cid, u64, Vec<Block>)>, Error> {
1383 let Some(mut leaves) = self.prefix_leaves(file_cid, filesize, boundary).await? else {
1384 return Ok(None);
1385 };
1386
1387 let mut new_blocks = Vec::new();
1388 for chunk in new_tail.chunks(CHUNK as usize) {
1389 let cid = Cid::new_v1(RAW_LEAF_CODEC, HASHER.digest(chunk));
1390 leaves.push(FileBranchLink {
1391 cid,
1392 blocksize: chunk.len() as u64,
1393 tsize: chunk.len() as u64,
1394 });
1395 new_blocks.push(Block::new(cid, chunk.to_vec())?);
1396 }
1397
1398 if leaves.is_empty() {
1399 let (cid, tsize, blocks) = encode_file(&[])?;
1400 return Ok(Some((cid, tsize, blocks)));
1401 }
1402
1403 let (root_cid, root_tsize, branch_blocks) =
1404 build_file_from_leaves(&leaves, VERSION, HASHER);
1405 for (cid, bytes) in branch_blocks {
1406 new_blocks.push(Block::new(cid, bytes)?);
1407 }
1408 Ok(Some((root_cid, root_tsize, new_blocks)))
1409 }
1410
1411 async fn grow_file(
1412 &self,
1413 file_cid: Cid,
1414 filesize: u64,
1415 offset: u64,
1416 data: &[u8],
1417 ) -> Result<Option<(Cid, u64, Vec<Block>)>, Error> {
1418 let boundary = (offset.min(filesize) / CHUNK) * CHUNK;
1419 let _gc = self.repo().gc_guard().await;
1420 let mut new_tail = if boundary < filesize {
1421 self.read_file_range(&file_cid, boundary, filesize).await?
1422 } else {
1423 Vec::new()
1424 };
1425 let rel_off = (offset - boundary) as usize;
1426 let rel_end = rel_off + data.len();
1427 if new_tail.len() < rel_end {
1428 new_tail.resize(rel_end, 0);
1429 }
1430 new_tail[rel_off..rel_end].copy_from_slice(data);
1431 self.rebuild_from_boundary(file_cid, filesize, boundary, new_tail)
1432 .await
1433 }
1434
1435 async fn read_existing_file_locked(
1436 &self,
1437 guard: &mut (bool, Option<Cid>),
1438 comps: &[String],
1439 ) -> Result<Option<Vec<u8>>, Error> {
1440 let Some(root) = self.cached_root(guard).await? else {
1441 return Ok(None);
1442 };
1443 let _gc = self.repo().gc_guard().await;
1444 let (cid, tsize) = match self.resolve_from(root, comps).await {
1445 Ok(resolved) => resolved,
1446 Err(_) => return Ok(None),
1447 };
1448 match self.classify(&cid, tsize).await? {
1449 MfsKind::Directory => {
1450 return Err(MfsError::IsDirectory(format!("/{}", comps.join("/"))).into());
1451 }
1452 MfsKind::Symlink => {
1453 return Err(MfsError::IsSymlink(format!("/{}", comps.join("/"))).into());
1454 }
1455 MfsKind::File { .. } => {}
1456 }
1457 Ok(Some(self.read_file(&cid).await?))
1458 }
1459
1460 async fn read_file(&self, cid: &Cid) -> Result<Vec<u8>, Error> {
1463 let block = self.get_block(cid).await?;
1464
1465 if matches!(describe(block.data()), NodeDescription::Other) {
1467 return Ok(block.data().to_vec());
1468 }
1469
1470 let mut out = Vec::new();
1471 let mut cache = None;
1472 let (content, _, _, mut step) = IdleFileVisit::default().start(block.data())?;
1473 out.extend_from_slice(content);
1474
1475 while let Some(visit) = step {
1476 let next = *visit.pending_links().0;
1477 let block = self.get_block(&next).await?;
1478 let (content, next_step) = visit.continue_walk(block.data(), &mut cache)?;
1479 out.extend_from_slice(content);
1480 step = next_step;
1481 }
1482
1483 Ok(out)
1484 }
1485
1486 async fn get_block(&self, cid: &Cid) -> Result<Block, Error> {
1487 self.repo()
1488 .get_block_now(cid)
1489 .await?
1490 .ok_or_else(|| anyhow!("missing block {cid}"))
1491 }
1492}
1493
1494fn split_path(path: &str) -> Result<Vec<String>, Error> {
1496 let mut comps = Vec::new();
1497 for segment in path.split('/') {
1498 match segment {
1499 "" => continue,
1500 "." | ".." => return Err(anyhow!("'.' and '..' are not supported in MFS paths")),
1501 other => comps.push(other.to_string()),
1502 }
1503 }
1504 Ok(comps)
1505}
1506
1507fn entry_size(kind: MfsKind, cumulative: u64) -> u64 {
1509 match kind {
1510 MfsKind::File { size } => size,
1511 MfsKind::Directory | MfsKind::Symlink => cumulative,
1512 }
1513}
1514
1515fn base_name(path: &str) -> Option<&str> {
1517 path.rsplit('/').find(|s| !s.is_empty())
1518}
1519
1520fn is_not_found(err: &Error) -> bool {
1521 matches!(err.downcast_ref::<MfsError>(), Some(MfsError::NotFound(_)))
1522}
1523
1524fn is_shard_prefix(name: &str) -> bool {
1525 name.len() == 2 && name.bytes().all(|b| b.is_ascii_hexdigit())
1526}
1527
1528fn is_ipfs_path(path: &str) -> bool {
1529 path.starts_with("/ipfs/") || path.starts_with("/ipld/")
1530}
1531
1532fn links_to_map(links: Vec<DirLink>) -> DirMap {
1533 links
1534 .into_iter()
1535 .map(|l| {
1536 (
1537 l.name,
1538 DirEntry {
1539 cid: l.target,
1540 tsize: l.tsize,
1541 },
1542 )
1543 })
1544 .collect()
1545}
1546
1547fn encode_dir(
1548 entries: &DirMap,
1549 shard_threshold: Option<u64>,
1550) -> Result<(Cid, u64, Vec<Block>), Error> {
1551 let mut opts = TreeOptions::default();
1552 opts.wrap_with_directory();
1553 opts.cid_version(VERSION);
1554 opts.hasher(HASHER);
1555 opts.shard_threshold(shard_threshold);
1556
1557 let mut builder = BufferingTreeBuilder::new(opts);
1558 for (name, entry) in entries {
1559 builder.put_link(name, entry.cid, entry.tsize)?;
1560 }
1561
1562 let mut blocks = Vec::new();
1563 let mut root = None;
1564 for node in builder.build() {
1565 let node = node?;
1566 root = Some((node.cid, node.total_size));
1567 blocks.push(Block::new(node.cid, node.block.into_vec())?);
1568 }
1569
1570 let (cid, tsize) = root.ok_or_else(|| anyhow!("directory produced no node"))?;
1571 Ok((cid, tsize, blocks))
1572}
1573
1574fn encode_file(data: &[u8]) -> Result<(Cid, u64, Vec<Block>), Error> {
1577 let mut adder = FileAdder::builder()
1578 .with_cid_version(VERSION)
1579 .with_hasher(HASHER)
1580 .build();
1581
1582 let mut blocks = Vec::new();
1583 let mut tsize = 0u64;
1584 let mut root = None;
1585
1586 let mut push = |cid: Cid, block: Vec<u8>| -> Result<(), Error> {
1587 tsize += block.len() as u64;
1588 root = Some(cid);
1589 blocks.push(Block::new(cid, block)?);
1590 Ok(())
1591 };
1592
1593 let mut offset = 0;
1594 while offset < data.len() {
1595 let (ready, consumed) = adder.push(&data[offset..]);
1596 for (cid, block) in ready {
1597 push(cid, block)?;
1598 }
1599 offset += consumed;
1600 }
1601 for (cid, block) in adder.finish() {
1602 push(cid, block)?;
1603 }
1604
1605 let cid = root.ok_or_else(|| anyhow!("file produced no blocks"))?;
1606 Ok((cid, tsize, blocks))
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611 use super::*;
1612
1613 async fn mfs() -> Mfs {
1614 let repo = Repo::new_memory();
1615 repo.init().await.unwrap();
1616 Mfs::new(repo)
1617 }
1618
1619 #[tokio::test]
1620 async fn mkdir_ls_stat() {
1621 let mfs = mfs().await;
1622
1623 mfs.mkdir("/a/b/c", true).await.unwrap();
1624
1625 let top = mfs.ls("/").await.unwrap();
1626 assert_eq!(top.len(), 1);
1627 assert_eq!(top[0].name, "a");
1628 assert_eq!(top[0].kind, MfsKind::Directory);
1629
1630 let inner = mfs.ls("/a/b").await.unwrap();
1631 assert_eq!(inner.len(), 1);
1632 assert_eq!(inner[0].name, "c");
1633
1634 let st = mfs.stat("/a/b/c").await.unwrap();
1635 assert_eq!(st.kind, MfsKind::Directory);
1636 }
1637
1638 #[tokio::test]
1639 async fn empty_root_lists_empty() {
1640 let mfs = mfs().await;
1641 assert!(mfs.ls("/").await.unwrap().is_empty());
1642 assert_eq!(mfs.stat("/").await.unwrap().kind, MfsKind::Directory);
1643 assert!(
1644 mfs.root().await.unwrap().is_none(),
1645 "reads must not create a root"
1646 );
1647 }
1648
1649 #[tokio::test]
1650 async fn write_read_roundtrip() {
1651 let mfs = mfs().await;
1652
1653 mfs.write("/docs/hello.txt", b"hello mfs", true)
1654 .await
1655 .unwrap();
1656 assert_eq!(mfs.read("/docs/hello.txt").await.unwrap(), b"hello mfs");
1657
1658 let st = mfs.stat("/docs/hello.txt").await.unwrap();
1659 assert_eq!(st.kind, MfsKind::File { size: 9 });
1660
1661 mfs.write("/docs/hello.txt", b"changed", false)
1663 .await
1664 .unwrap();
1665 assert_eq!(mfs.read("/docs/hello.txt").await.unwrap(), b"changed");
1666
1667 let entries = mfs.ls("/docs").await.unwrap();
1669 assert_eq!(entries.len(), 1);
1670 assert_eq!(entries[0].name, "hello.txt");
1671 assert!(matches!(entries[0].kind, MfsKind::File { .. }));
1672 }
1673
1674 #[tokio::test]
1675 async fn write_at_offset_and_truncate() {
1676 let mfs = mfs().await;
1677 mfs.write("/f", b"hello world", true).await.unwrap();
1678
1679 mfs.write_with(
1680 "/f",
1681 b"MFS",
1682 WriteOptions {
1683 offset: 6,
1684 ..Default::default()
1685 },
1686 )
1687 .await
1688 .unwrap();
1689 assert_eq!(mfs.read("/f").await.unwrap(), b"hello MFSld");
1690
1691 mfs.write_with(
1692 "/f",
1693 b"!!",
1694 WriteOptions {
1695 offset: 13,
1696 ..Default::default()
1697 },
1698 )
1699 .await
1700 .unwrap();
1701 assert_eq!(mfs.read("/f").await.unwrap(), b"hello MFSld\0\0!!");
1702
1703 mfs.write_with(
1704 "/f",
1705 b"fresh",
1706 WriteOptions {
1707 truncate: true,
1708 ..Default::default()
1709 },
1710 )
1711 .await
1712 .unwrap();
1713 assert_eq!(mfs.read("/f").await.unwrap(), b"fresh");
1714
1715 mfs.truncate("/f", 3).await.unwrap();
1716 assert_eq!(mfs.read("/f").await.unwrap(), b"fre");
1717 mfs.truncate("/f", 5).await.unwrap();
1718 assert_eq!(mfs.read("/f").await.unwrap(), b"fre\0\0");
1719
1720 assert!(
1721 mfs.write_with("/missing", b"x", WriteOptions::default())
1722 .await
1723 .is_err()
1724 );
1725 mfs.write_with(
1726 "/created",
1727 b"y",
1728 WriteOptions {
1729 create: true,
1730 parents: true,
1731 ..Default::default()
1732 },
1733 )
1734 .await
1735 .unwrap();
1736 assert_eq!(mfs.read("/created").await.unwrap(), b"y");
1737 }
1738
1739 #[tokio::test]
1740 async fn offset_edit_multiblock() {
1741 let mfs = mfs().await;
1742 let mut data: Vec<u8> = (0..1_000_000u32).map(|i| i as u8).collect();
1743 mfs.write("/big", &data, false).await.unwrap();
1744
1745 let patch = b"PATCHED";
1746 let off = 500_000usize;
1747 mfs.write_with(
1748 "/big",
1749 patch,
1750 WriteOptions {
1751 offset: off as u64,
1752 ..Default::default()
1753 },
1754 )
1755 .await
1756 .unwrap();
1757
1758 data[off..off + patch.len()].copy_from_slice(patch);
1759 assert_eq!(mfs.read("/big").await.unwrap(), data);
1760 }
1761
1762 #[tokio::test]
1763 async fn overwrite_in_place_is_canonical_and_reuses_blocks() {
1764 use futures::StreamExt as _;
1765
1766 let repo = Repo::new_memory();
1767 repo.init().await.unwrap();
1768 let mfs = Mfs::new(repo.clone());
1769
1770 let mut content = vec![0u8; 4_000_000];
1771 let mut x = 0x1234_5678u32;
1772 for b in content.iter_mut() {
1773 x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1774 *b = (x >> 24) as u8;
1775 }
1776 mfs.write("/big", &content, false).await.unwrap();
1777
1778 let before = repo.list_blocks().await.collect::<Vec<_>>().await.len();
1779 assert!(before > 8, "file should be many blocks, got {before}");
1780
1781 let patch = b"PATCHED-IN-PLACE";
1782 let off = 1_500_000u64;
1783 mfs.write_with(
1784 "/big",
1785 patch,
1786 WriteOptions {
1787 offset: off,
1788 ..Default::default()
1789 },
1790 )
1791 .await
1792 .unwrap();
1793
1794 content[off as usize..off as usize + patch.len()].copy_from_slice(patch);
1795 let (expected_cid, _, _) = encode_file(&content).unwrap();
1796 assert_eq!(
1797 mfs.stat("/big").await.unwrap().cid,
1798 expected_cid,
1799 "editor must produce the canonical tree"
1800 );
1801 assert_eq!(mfs.read("/big").await.unwrap(), content);
1802
1803 let after = repo.list_blocks().await.collect::<Vec<_>>().await.len();
1804 assert!(
1805 after - before < 8,
1806 "expected few new blocks (reuse), added {}",
1807 after - before
1808 );
1809
1810 let patch2 = b"CROSS-CHUNK-BOUNDARY";
1811 let off2 = 262_144usize - 5;
1812 mfs.write_with(
1813 "/big",
1814 patch2,
1815 WriteOptions {
1816 offset: off2 as u64,
1817 ..Default::default()
1818 },
1819 )
1820 .await
1821 .unwrap();
1822 content[off2..off2 + patch2.len()].copy_from_slice(patch2);
1823 let (expected2, _, _) = encode_file(&content).unwrap();
1824 assert_eq!(mfs.stat("/big").await.unwrap().cid, expected2);
1825 assert_eq!(mfs.read("/big").await.unwrap(), content);
1826 }
1827
1828 fn fill(n: usize, seed: u32) -> Vec<u8> {
1829 let mut v = vec![0u8; n];
1830 let mut x = seed;
1831 for b in v.iter_mut() {
1832 x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1833 *b = (x >> 24) as u8;
1834 }
1835 v
1836 }
1837
1838 async fn assert_canonical(mfs: &Mfs, path: &str, expected: &[u8]) {
1839 let (cid, _, _) = encode_file(expected).unwrap();
1840 assert_eq!(
1841 mfs.stat(path).await.unwrap().cid,
1842 cid,
1843 "non-canonical tree for {path}"
1844 );
1845 assert_eq!(mfs.read(path).await.unwrap(), expected);
1846 }
1847
1848 #[tokio::test]
1849 async fn grow_append_truncate_are_canonical() {
1850 use futures::StreamExt as _;
1851 let repo = Repo::new_memory();
1852 repo.init().await.unwrap();
1853 let mfs = Mfs::new(repo.clone());
1854
1855 let mut content = fill(800_000, 1);
1856 mfs.write("/f", &content, true).await.unwrap();
1857 let before = repo.list_blocks().await.collect::<Vec<_>>().await.len();
1858
1859 let app = fill(300_000, 2);
1860 mfs.write_with(
1861 "/f",
1862 &app,
1863 WriteOptions {
1864 offset: content.len() as u64,
1865 ..Default::default()
1866 },
1867 )
1868 .await
1869 .unwrap();
1870 content.extend_from_slice(&app);
1871 assert_canonical(&mfs, "/f", &content).await;
1872 let after = repo.list_blocks().await.collect::<Vec<_>>().await.len();
1873 assert!(
1874 after - before < 8,
1875 "append should reuse, added {}",
1876 after - before
1877 );
1878
1879 let patch = fill(200_000, 3);
1880 let off = content.len() - 50_000;
1881 mfs.write_with(
1882 "/f",
1883 &patch,
1884 WriteOptions {
1885 offset: off as u64,
1886 ..Default::default()
1887 },
1888 )
1889 .await
1890 .unwrap();
1891 content.resize((off + patch.len()).max(content.len()), 0);
1892 content[off..off + patch.len()].copy_from_slice(&patch);
1893 assert_canonical(&mfs, "/f", &content).await;
1894
1895 mfs.truncate("/f", 600_000).await.unwrap();
1896 content.truncate(600_000);
1897 assert_canonical(&mfs, "/f", &content).await;
1898
1899 mfs.truncate("/f", 900_000).await.unwrap();
1900 content.resize(900_000, 0);
1901 assert_canonical(&mfs, "/f", &content).await;
1902
1903 mfs.truncate("/f", 0).await.unwrap();
1904 assert_canonical(&mfs, "/f", &[]).await;
1905 }
1906
1907 #[tokio::test]
1908 async fn small_file_grows_to_multiblock() {
1909 let mfs = mfs().await;
1910 mfs.write("/s", b"small", true).await.unwrap();
1911
1912 let app = fill(600_000, 9);
1913 mfs.write_with(
1914 "/s",
1915 &app,
1916 WriteOptions {
1917 offset: 5,
1918 ..Default::default()
1919 },
1920 )
1921 .await
1922 .unwrap();
1923
1924 let mut content = b"small".to_vec();
1925 content.resize(5 + app.len(), 0);
1926 content[5..5 + app.len()].copy_from_slice(&app);
1927 assert_canonical(&mfs, "/s", &content).await;
1928 }
1929
1930 #[tokio::test]
1931 async fn write_stream_is_canonical() {
1932 let mfs = mfs().await;
1933
1934 let content = fill(700_000, 11);
1935 let chunks: Vec<bytes::Bytes> = content
1936 .chunks(33_333)
1937 .map(bytes::Bytes::copy_from_slice)
1938 .collect();
1939 let stream = futures::stream::iter(chunks.into_iter().map(Ok::<_, std::io::Error>));
1940 mfs.write_stream("/streamed", stream, true).await.unwrap();
1941 assert_canonical(&mfs, "/streamed", &content).await;
1942
1943 let empty = futures::stream::iter(Vec::<std::io::Result<bytes::Bytes>>::new());
1944 mfs.write_stream("/empty", empty, false).await.unwrap();
1945 assert_canonical(&mfs, "/empty", &[]).await;
1946 }
1947
1948 #[tokio::test]
1949 async fn write_stream_reports_progress() {
1950 use futures::StreamExt as _;
1951 let mfs = mfs().await;
1952
1953 let content = fill(700_000, 12);
1954 let chunks: Vec<bytes::Bytes> = content
1955 .chunks(50_000)
1956 .map(bytes::Bytes::copy_from_slice)
1957 .collect();
1958 let stream = futures::stream::iter(chunks.into_iter().map(Ok::<_, std::io::Error>));
1959
1960 let mut write = mfs.write_stream("/big", stream, true);
1961 let mut last = 0u64;
1962 let mut completed = None;
1963 while let Some(status) = write.next().await {
1964 match status {
1965 WriteStatus::Progress { written, total } => {
1966 assert!(written >= last);
1967 assert_eq!(total, None);
1968 last = written;
1969 }
1970 WriteStatus::Completed { cid } => completed = Some(cid),
1971 WriteStatus::Failed { error } => panic!("write failed: {error}"),
1972 }
1973 }
1974 let (expected, _, _) = encode_file(&content).unwrap();
1975 assert_eq!(completed, Some(expected));
1976 assert_eq!(last, content.len() as u64);
1977 assert_canonical(&mfs, "/big", &content).await;
1978 }
1979
1980 #[tokio::test]
1981 async fn write_from_file_roundtrips_with_total() {
1982 use futures::StreamExt as _;
1983 let mfs = mfs().await;
1984
1985 let content = fill(400_000, 13);
1986 let file = std::env::temp_dir().join("rust_ipfs_mfs_write_from_file_test.bin");
1987 std::fs::write(&file, &content).unwrap();
1988
1989 let cid = mfs.write_from_file("/imported", &file, true).await.unwrap();
1990 let (expected, _, _) = encode_file(&content).unwrap();
1991 assert_eq!(cid, expected);
1992 assert_canonical(&mfs, "/imported", &content).await;
1993
1994 let mut write = mfs.write_from_file("/imported2", &file, true);
1995 let mut saw_total = false;
1996 while let Some(status) = write.next().await {
1997 if let WriteStatus::Progress { total, .. } = status {
1998 assert_eq!(total, Some(content.len() as u64));
1999 saw_total = true;
2000 }
2001 }
2002 assert!(saw_total);
2003
2004 assert!(
2005 mfs.write_from_file("/x", file.join("missing"), false)
2006 .await
2007 .is_err()
2008 );
2009
2010 std::fs::remove_file(&file).ok();
2011 }
2012
2013 #[tokio::test]
2014 async fn read_stream_roundtrips() {
2015 use futures::StreamExt as _;
2016 let mfs = mfs().await;
2017 let content = fill(600_000, 21);
2018 mfs.write("/f", &content, true).await.unwrap();
2019
2020 let stream = mfs.read_stream("/f");
2021 futures::pin_mut!(stream);
2022 let mut out = Vec::new();
2023 while let Some(chunk) = stream.next().await {
2024 out.extend_from_slice(&chunk.unwrap());
2025 }
2026 assert_eq!(out, content);
2027
2028 mfs.mkdir("/d", false).await.unwrap();
2029 let dir_stream = mfs.read_stream("/d");
2030 futures::pin_mut!(dir_stream);
2031 assert!(dir_stream.next().await.unwrap().is_err());
2032 }
2033
2034 #[tokio::test]
2035 async fn read_to_file_roundtrips_with_progress() {
2036 use futures::StreamExt as _;
2037 let mfs = mfs().await;
2038 let content = fill(500_000, 22);
2039 mfs.write("/f", &content, true).await.unwrap();
2040
2041 let dest = std::env::temp_dir().join("rust_ipfs_mfs_read_to_file_test.bin");
2042 std::fs::remove_file(&dest).ok();
2043
2044 let written = mfs.read_to_file("/f", &dest).await.unwrap();
2045 assert_eq!(written, content.len() as u64);
2046 assert_eq!(std::fs::read(&dest).unwrap(), content);
2047
2048 let mut read = mfs.read_to_file("/f", &dest);
2049 let mut last = 0u64;
2050 while let Some(status) = read.next().await {
2051 match status {
2052 ReadStatus::Progress { written, total } => {
2053 assert!(written >= last);
2054 assert_eq!(total, content.len() as u64);
2055 last = written;
2056 }
2057 ReadStatus::Completed { written } => assert_eq!(written, content.len() as u64),
2058 ReadStatus::Failed { error } => panic!("read failed: {error}"),
2059 }
2060 }
2061 assert_eq!(last, content.len() as u64);
2062
2063 std::fs::remove_file(&dest).ok();
2064 }
2065
2066 #[tokio::test]
2067 async fn empty_file_grow_is_canonical() {
2068 let mfs = mfs().await;
2069
2070 mfs.write("/e", b"", true).await.unwrap();
2071 let data = fill(CHUNK as usize + 5, 4);
2072 mfs.write_with(
2073 "/e",
2074 &data,
2075 WriteOptions {
2076 offset: 0,
2077 create: true,
2078 ..Default::default()
2079 },
2080 )
2081 .await
2082 .unwrap();
2083 assert_canonical(&mfs, "/e", &data).await;
2084
2085 mfs.write("/t", &fill(500_000, 5), true).await.unwrap();
2086 mfs.truncate("/t", 0).await.unwrap();
2087 let grow = fill(CHUNK as usize + 5, 6);
2088 mfs.write_with(
2089 "/t",
2090 &grow,
2091 WriteOptions {
2092 offset: 0,
2093 ..Default::default()
2094 },
2095 )
2096 .await
2097 .unwrap();
2098 assert_canonical(&mfs, "/t", &grow).await;
2099 }
2100
2101 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2102 async fn concurrent_writes_to_same_file_dont_lose_updates() {
2103 let repo = Repo::new_memory();
2104 repo.init().await.unwrap();
2105 let mfs = Mfs::new(repo);
2106 mfs.write("/f", &vec![0u8; 2000], true).await.unwrap();
2107
2108 let a = mfs.clone();
2109 let b = mfs.clone();
2110 let ha = tokio::spawn(async move {
2111 a.write_with(
2112 "/f",
2113 &[1, 1, 1, 1],
2114 WriteOptions {
2115 offset: 0,
2116 ..Default::default()
2117 },
2118 )
2119 .await
2120 });
2121 let hb = tokio::spawn(async move {
2122 b.write_with(
2123 "/f",
2124 &[2, 2, 2, 2],
2125 WriteOptions {
2126 offset: 1000,
2127 ..Default::default()
2128 },
2129 )
2130 .await
2131 });
2132 ha.await.unwrap().unwrap();
2133 hb.await.unwrap().unwrap();
2134
2135 let content = mfs.read("/f").await.unwrap();
2136 assert_eq!(&content[0..4], &[1, 1, 1, 1], "write A was lost");
2137 assert_eq!(&content[1000..1004], &[2, 2, 2, 2], "write B was lost");
2138 assert_eq!(content.len(), 2000);
2139 }
2140
2141 #[tokio::test]
2142 async fn imported_nonstandard_chunk_file_falls_back() {
2143 use rust_unixfs::file::adder::{Chunker, FileAdder};
2144 let repo = Repo::new_memory();
2145 repo.init().await.unwrap();
2146 let mfs = Mfs::new(repo.clone());
2147
2148 let content = fill(900_000, 7);
2150 let mut adder = FileAdder::builder()
2151 .with_chunker(Chunker::Size(400 * 1024))
2152 .with_cid_version(VERSION)
2153 .with_hasher(HASHER)
2154 .build();
2155 let mut blocks = Vec::new();
2156 let mut root = None;
2157 let mut off = 0;
2158 while off < content.len() {
2159 let (ready, consumed) = adder.push(&content[off..]);
2160 for (cid, block) in ready {
2161 root = Some(cid);
2162 blocks.push(Block::new(cid, block).unwrap());
2163 }
2164 off += consumed;
2165 }
2166 for (cid, block) in adder.finish() {
2167 root = Some(cid);
2168 blocks.push(Block::new(cid, block).unwrap());
2169 }
2170 let root = root.unwrap();
2171 repo.put_blocks(blocks).await.unwrap();
2172
2173 mfs.cp(&format!("/ipfs/{root}"), "/imported", true)
2174 .await
2175 .unwrap();
2176
2177 let app = fill(100_000, 8);
2180 let mut expected = content.clone();
2181 expected.extend_from_slice(&app);
2182 mfs.write_with(
2183 "/imported",
2184 &app,
2185 WriteOptions {
2186 offset: content.len() as u64,
2187 ..Default::default()
2188 },
2189 )
2190 .await
2191 .unwrap();
2192 assert_canonical(&mfs, "/imported", &expected).await;
2193 }
2194
2195 #[tokio::test]
2196 async fn write_read_multiblock() {
2197 let mfs = mfs().await;
2198 let data: Vec<u8> = (0..1_000_000u32).map(|i| i as u8).collect();
2200
2201 mfs.write("/big.bin", &data, false).await.unwrap();
2202 assert_eq!(mfs.read("/big.bin").await.unwrap(), data);
2203
2204 match mfs.stat("/big.bin").await.unwrap().kind {
2205 MfsKind::File { size } => assert_eq!(size, data.len() as u64),
2206 other => panic!("expected file, got {other:?}"),
2207 }
2208 }
2209
2210 #[tokio::test]
2211 async fn large_dir_shards_and_roundtrips() {
2212 let repo = Repo::new_memory();
2213 repo.init().await.unwrap();
2214 let mfs = Mfs::new(repo).with_shard_threshold(Some(0));
2215
2216 let n = 64usize;
2217 for i in 0..n {
2218 mfs.write(
2219 &format!("/d/file{i:03}"),
2220 format!("content {i}").as_bytes(),
2221 true,
2222 )
2223 .await
2224 .unwrap();
2225 }
2226
2227 let cid = mfs.stat("/d").await.unwrap().cid;
2228 let block = mfs.get_block(&cid).await.unwrap();
2229 assert!(
2230 matches!(describe(block.data()), NodeDescription::HamtShard { .. }),
2231 "directory should be HAMT-sharded"
2232 );
2233
2234 let entries = mfs.ls("/d").await.unwrap();
2235 assert_eq!(entries.len(), n);
2236
2237 assert_eq!(mfs.read("/d/file007").await.unwrap(), b"content 7");
2238 assert_eq!(mfs.read("/d/file063").await.unwrap(), b"content 63");
2239
2240 mfs.write("/d/file064", b"new", false).await.unwrap();
2241 assert_eq!(mfs.ls("/d").await.unwrap().len(), n + 1);
2242
2243 mfs.rm("/d/file007", false).await.unwrap();
2244 assert_eq!(mfs.ls("/d").await.unwrap().len(), n);
2245 assert!(mfs.read("/d/file007").await.is_err());
2246 }
2247
2248 #[tokio::test]
2249 async fn cp_from_ipfs_through_hamt_dir() {
2250 let repo = Repo::new_memory();
2251 repo.init().await.unwrap();
2252 let mfs = Mfs::new(repo).with_shard_threshold(Some(0));
2253
2254 for i in 0..64u32 {
2255 mfs.write(
2256 &format!("/d/file{i:03}"),
2257 format!("content {i}").as_bytes(),
2258 true,
2259 )
2260 .await
2261 .unwrap();
2262 }
2263
2264 let dir_cid = mfs.stat("/d").await.unwrap().cid;
2265 let block = mfs.get_block(&dir_cid).await.unwrap();
2266 assert!(
2267 matches!(describe(block.data()), NodeDescription::HamtShard { .. }),
2268 "source dir should be HAMT-sharded"
2269 );
2270
2271 mfs.cp(&format!("/ipfs/{dir_cid}/file042"), "/copied", false)
2272 .await
2273 .unwrap();
2274 assert_eq!(mfs.read("/copied").await.unwrap(), b"content 42");
2275
2276 assert!(
2277 mfs.cp(&format!("/ipfs/{dir_cid}/missing"), "/x", false)
2278 .await
2279 .is_err()
2280 );
2281 }
2282
2283 #[tokio::test]
2284 async fn cp_from_ipfs() {
2285 let repo = Repo::new_memory();
2286 repo.init().await.unwrap();
2287 let mfs = Mfs::new(repo.clone());
2288
2289 let (file_cid, _, blocks) = encode_file(b"imported content").unwrap();
2290 repo.put_blocks(blocks).await.unwrap();
2291
2292 mfs.cp(&format!("/ipfs/{file_cid}"), "/imported.txt", true)
2293 .await
2294 .unwrap();
2295 assert_eq!(
2296 mfs.read("/imported.txt").await.unwrap(),
2297 b"imported content"
2298 );
2299 assert!(matches!(
2300 mfs.stat("/imported.txt").await.unwrap().kind,
2301 MfsKind::File { .. }
2302 ));
2303
2304 let (greeting_cid, greeting_tsize, gblocks) = encode_file(b"hi").unwrap();
2305 repo.put_blocks(gblocks).await.unwrap();
2306 let mut dirmap = DirMap::new();
2307 dirmap.insert(
2308 "greeting".into(),
2309 DirEntry {
2310 cid: greeting_cid,
2311 tsize: greeting_tsize,
2312 },
2313 );
2314 let (dir_cid, _, dblocks) = encode_dir(&dirmap, None).unwrap();
2315 repo.put_blocks(dblocks).await.unwrap();
2316
2317 mfs.cp(&format!("/ipfs/{dir_cid}"), "/srcdir", false)
2318 .await
2319 .unwrap();
2320 assert_eq!(mfs.ls("/srcdir").await.unwrap().len(), 1);
2321
2322 mfs.cp(&format!("/ipfs/{dir_cid}/greeting"), "/hi.txt", false)
2323 .await
2324 .unwrap();
2325 assert_eq!(mfs.read("/hi.txt").await.unwrap(), b"hi");
2326 }
2327
2328 #[tokio::test]
2329 async fn rm_cp_mv() {
2330 let mfs = mfs().await;
2331 mfs.write("/a/file", b"data", true).await.unwrap();
2332
2333 mfs.cp("/a/file", "/a/copy", false).await.unwrap();
2335 assert_eq!(mfs.read("/a/copy").await.unwrap(), b"data");
2336 assert_eq!(mfs.read("/a/file").await.unwrap(), b"data");
2337
2338 mfs.mkdir("/b", false).await.unwrap();
2340 mfs.mv("/a/copy", "/b/moved", false).await.unwrap();
2341 assert_eq!(mfs.read("/b/moved").await.unwrap(), b"data");
2342 assert!(mfs.read("/a/copy").await.is_err());
2343
2344 mfs.rm("/a/file", false).await.unwrap();
2346 assert!(mfs.read("/a/file").await.is_err());
2347
2348 assert!(mfs.rm("/b", false).await.is_err());
2350 mfs.rm("/b", true).await.unwrap();
2351 assert!(mfs.ls("/b").await.is_err());
2352 }
2353
2354 #[tokio::test]
2355 async fn mkdir_without_parents_errors_on_missing_parent() {
2356 let mfs = mfs().await;
2357 assert!(mfs.mkdir("/x/y", false).await.is_err());
2358 mfs.mkdir("/x", false).await.unwrap();
2359 mfs.mkdir("/x/y", false).await.unwrap();
2360 assert!(
2361 mfs.mkdir("/x/y", false).await.is_err(),
2362 "duplicate mkdir must fail"
2363 );
2364 }
2365
2366 #[tokio::test]
2367 async fn mfs_tree_survives_gc() {
2368 let repo = Repo::new_memory();
2369 repo.init().await.unwrap();
2370 let mfs = Mfs::new(repo.clone());
2371
2372 mfs.write("/a/keep.txt", b"survive gc", true).await.unwrap();
2373
2374 repo.cleanup().await.unwrap();
2376
2377 assert_eq!(mfs.read("/a/keep.txt").await.unwrap(), b"survive gc");
2378 assert_eq!(mfs.ls("/a").await.unwrap().len(), 1);
2379 }
2380
2381 #[tokio::test]
2382 async fn root_persists_across_handles() {
2383 let repo = Repo::new_memory();
2384 repo.init().await.unwrap();
2385
2386 let root_cid = {
2387 let mfs = Mfs::new(repo.clone());
2388 mfs.mkdir("/keep", true).await.unwrap();
2389 mfs.root().await.unwrap().unwrap()
2390 };
2391
2392 let reopened = Mfs::new(repo);
2394 assert_eq!(reopened.root().await.unwrap(), Some(root_cid));
2395 let entries = reopened.ls("/").await.unwrap();
2396 assert_eq!(entries.len(), 1);
2397 assert_eq!(entries[0].name, "keep");
2398 }
2399
2400 #[tokio::test]
2401 async fn read_range_partial() {
2402 let mfs = mfs().await;
2403 let content = fill(600_000, 41);
2404 mfs.write("/f", &content, true).await.unwrap();
2405
2406 assert_eq!(
2407 mfs.read_range("/f", 100, Some(50)).await.unwrap(),
2408 content[100..150]
2409 );
2410 assert_eq!(
2411 mfs.read_range("/f", 261_000, Some(2000)).await.unwrap(),
2412 content[261_000..263_000],
2413 "range crossing a chunk boundary"
2414 );
2415 assert_eq!(
2416 mfs.read_range("/f", 590_000, None).await.unwrap(),
2417 content[590_000..]
2418 );
2419 assert_eq!(mfs.read_range("/f", 0, None).await.unwrap(), content);
2420 assert_eq!(
2421 mfs.read_range("/f", 599_990, Some(1000)).await.unwrap(),
2422 content[599_990..],
2423 "count past EOF clamps"
2424 );
2425 assert!(
2426 mfs.read_range("/f", 600_000, Some(10))
2427 .await
2428 .unwrap()
2429 .is_empty()
2430 );
2431 assert!(
2432 mfs.read_range("/f", 10_000_000, Some(5))
2433 .await
2434 .unwrap()
2435 .is_empty()
2436 );
2437
2438 mfs.write("/s", b"hello", true).await.unwrap();
2439 assert_eq!(mfs.read_range("/s", 1, Some(3)).await.unwrap(), b"ell");
2440 assert_eq!(mfs.read_range("/s", 2, None).await.unwrap(), b"llo");
2441 assert!(mfs.read_range("/s", 9, Some(1)).await.unwrap().is_empty());
2442
2443 let stream = mfs.read_stream_range("/f", 1000, Some(500));
2444 futures::pin_mut!(stream);
2445 let mut out = Vec::new();
2446 while let Some(chunk) = stream.next().await {
2447 out.extend_from_slice(&chunk.unwrap());
2448 }
2449 assert_eq!(out, content[1000..1500]);
2450 }
2451
2452 #[tokio::test]
2453 async fn cp_mv_into_existing_directory() {
2454 let mfs = mfs().await;
2455 mfs.write("/src/file.txt", b"hi", true).await.unwrap();
2456
2457 mfs.mkdir("/dst", false).await.unwrap();
2458 mfs.cp("/src/file.txt", "/dst", false).await.unwrap();
2459 assert_eq!(mfs.read("/dst/file.txt").await.unwrap(), b"hi");
2460
2461 mfs.mkdir("/d2", false).await.unwrap();
2462 mfs.cp("/src/file.txt", "/d2/", false).await.unwrap();
2463 assert_eq!(
2464 mfs.read("/d2/file.txt").await.unwrap(),
2465 b"hi",
2466 "trailing slash targets the dir"
2467 );
2468
2469 mfs.mkdir("/dst3", false).await.unwrap();
2470 mfs.mv("/src/file.txt", "/dst3", false).await.unwrap();
2471 assert_eq!(mfs.read("/dst3/file.txt").await.unwrap(), b"hi");
2472 assert!(!mfs.exists("/src/file.txt").await.unwrap());
2473 }
2474
2475 #[tokio::test]
2476 async fn cp_onto_existing_errors_mv_replaces() {
2477 let mfs = mfs().await;
2478 mfs.write("/a.txt", b"aaa", true).await.unwrap();
2479 mfs.write("/b.txt", b"bbb", true).await.unwrap();
2480
2481 let err = mfs.cp("/a.txt", "/b.txt", false).await.unwrap_err();
2482 assert!(matches!(
2483 err.downcast_ref::<MfsError>(),
2484 Some(MfsError::AlreadyExists(_))
2485 ));
2486 assert_eq!(mfs.read("/b.txt").await.unwrap(), b"bbb");
2487
2488 mfs.mv("/a.txt", "/b.txt", false).await.unwrap();
2489 assert_eq!(mfs.read("/b.txt").await.unwrap(), b"aaa");
2490 assert!(!mfs.exists("/a.txt").await.unwrap());
2491 }
2492
2493 #[tokio::test]
2494 async fn mv_self_and_descendant_are_guarded() {
2495 let mfs = mfs().await;
2496 mfs.write("/d/f.txt", b"data", true).await.unwrap();
2497
2498 mfs.mv("/d/f.txt", "/d/f.txt", false).await.unwrap();
2499 assert_eq!(
2500 mfs.read("/d/f.txt").await.unwrap(),
2501 b"data",
2502 "self-move must not destroy"
2503 );
2504
2505 mfs.mv("/d/f.txt", "/d", false).await.unwrap();
2506 assert_eq!(
2507 mfs.read("/d/f.txt").await.unwrap(),
2508 b"data",
2509 "move into own parent is a no-op"
2510 );
2511
2512 mfs.mkdir("/d/sub", true).await.unwrap();
2513 assert!(
2514 mfs.mv("/d", "/d/sub/inner", false).await.is_err(),
2515 "into own descendant"
2516 );
2517 assert_eq!(
2518 mfs.read("/d/f.txt").await.unwrap(),
2519 b"data",
2520 "subtree preserved"
2521 );
2522 }
2523
2524 #[tokio::test]
2525 async fn rm_force_is_idempotent() {
2526 let mfs = mfs().await;
2527 mfs.write("/x.txt", b"x", true).await.unwrap();
2528
2529 mfs.rm_force("/x.txt", false).await.unwrap();
2530 assert!(!mfs.exists("/x.txt").await.unwrap());
2531 mfs.rm_force("/x.txt", false).await.unwrap();
2532 mfs.rm_force("/never/existed", false).await.unwrap();
2533 assert!(
2534 mfs.rm("/x.txt", false).await.is_err(),
2535 "plain rm still errors on missing"
2536 );
2537
2538 mfs.write("/dir/child", b"c", true).await.unwrap();
2539 assert!(
2540 mfs.rm_force("/dir", false).await.is_err(),
2541 "non-empty still needs recursive"
2542 );
2543 mfs.rm_force("/dir", true).await.unwrap();
2544 assert!(!mfs.exists("/dir").await.unwrap());
2545 }
2546
2547 #[tokio::test]
2548 async fn ls_on_file_lists_itself() {
2549 let mfs = mfs().await;
2550 mfs.write("/docs/f.txt", b"hello", true).await.unwrap();
2551
2552 let entries = mfs.ls("/docs/f.txt").await.unwrap();
2553 assert_eq!(entries.len(), 1);
2554 assert_eq!(entries[0].name, "f.txt");
2555 assert_eq!(entries[0].kind, MfsKind::File { size: 5 });
2556 assert_eq!(entries[0].size, 5);
2557 }
2558
2559 #[tokio::test]
2560 async fn exists_reports_presence() {
2561 let mfs = mfs().await;
2562 mfs.mkdir("/a/b", true).await.unwrap();
2563 mfs.write("/a/f.txt", b"x", true).await.unwrap();
2564
2565 assert!(mfs.exists("/").await.unwrap());
2566 assert!(mfs.exists("/a").await.unwrap());
2567 assert!(mfs.exists("/a/b").await.unwrap());
2568 assert!(mfs.exists("/a/f.txt").await.unwrap());
2569 assert!(!mfs.exists("/a/nope").await.unwrap());
2570 assert!(!mfs.exists("/nope/deep").await.unwrap());
2571 }
2572
2573 #[tokio::test]
2574 async fn stat_reports_cumulative_and_blocks() {
2575 let mfs = mfs().await;
2576 let content = fill(600_000, 51);
2577 mfs.write("/big.bin", &content, true).await.unwrap();
2578
2579 let root = mfs.stat("/").await.unwrap();
2580 assert_eq!(root.kind, MfsKind::Directory);
2581 assert!(root.size > 0, "root size must reflect its contents, not 0");
2582 assert_eq!(root.size, root.cumulative_size);
2583
2584 let st = mfs.stat("/big.bin").await.unwrap();
2585 assert_eq!(st.kind, MfsKind::File { size: 600_000 });
2586 assert!(st.cumulative_size > st.size, "dag carries link overhead");
2587 assert!(
2588 st.blocks > 1,
2589 "a multiblock file has multiple leaf links, got {}",
2590 st.blocks
2591 );
2592
2593 mfs.write("/s", b"hi", true).await.unwrap();
2594 assert_eq!(
2595 mfs.stat("/s").await.unwrap().blocks,
2596 0,
2597 "a raw leaf has no links"
2598 );
2599 }
2600
2601 #[tokio::test]
2602 async fn write_stream_error_mid_stream_leaves_path_untouched() {
2603 use futures::StreamExt as _;
2604 let mfs = mfs().await;
2605
2606 let chunks: Vec<std::io::Result<bytes::Bytes>> = vec![
2607 Ok(bytes::Bytes::from_static(b"good")),
2608 Err(std::io::Error::other("boom")),
2609 ];
2610 let mut write = mfs.write_stream("/partial", futures::stream::iter(chunks), false);
2611 let mut failed = 0;
2612 let mut completed = false;
2613 while let Some(status) = write.next().await {
2614 match status {
2615 WriteStatus::Failed { .. } => failed += 1,
2616 WriteStatus::Completed { .. } => completed = true,
2617 WriteStatus::Progress { .. } => {}
2618 }
2619 }
2620 assert_eq!(failed, 1);
2621 assert!(!completed);
2622 assert!(
2623 !mfs.exists("/partial").await.unwrap(),
2624 "failed write must not create the path"
2625 );
2626
2627 let chunks2: Vec<std::io::Result<bytes::Bytes>> = vec![
2628 Ok(bytes::Bytes::from_static(b"x")),
2629 Err(std::io::Error::other("boom")),
2630 ];
2631 assert!(
2632 mfs.write_stream("/partial2", futures::stream::iter(chunks2), false)
2633 .await
2634 .is_err()
2635 );
2636 assert!(!mfs.exists("/partial2").await.unwrap());
2637 }
2638
2639 fn encode_file_bf(data: &[u8], bf: usize) -> (Cid, Vec<Block>) {
2640 use rust_unixfs::file::adder::BalancedCollector;
2641 let mut adder = FileAdder::builder()
2642 .with_cid_version(VERSION)
2643 .with_hasher(HASHER)
2644 .with_collector(BalancedCollector::with_branching_factor(bf))
2645 .build();
2646 let mut blocks = Vec::new();
2647 let mut root = None;
2648 let mut off = 0;
2649 while off < data.len() {
2650 let (ready, consumed) = adder.push(&data[off..]);
2651 for (cid, block) in ready {
2652 root = Some(cid);
2653 blocks.push(Block::new(cid, block).unwrap());
2654 }
2655 off += consumed;
2656 }
2657 for (cid, block) in adder.finish() {
2658 root = Some(cid);
2659 blocks.push(Block::new(cid, block).unwrap());
2660 }
2661 (root.unwrap(), blocks)
2662 }
2663
2664 #[tokio::test]
2665 async fn deep_tree_editor_overwrites_and_grows() {
2666 let repo = Repo::new_memory();
2667 repo.init().await.unwrap();
2668 let mfs = Mfs::new(repo.clone());
2669
2670 let bf = 4;
2671 let content = fill(20 * CHUNK as usize, 31);
2672 let (root, blocks) = encode_file_bf(&content, bf);
2673 repo.put_blocks(blocks).await.unwrap();
2674 mfs.cp(&format!("/ipfs/{root}"), "/deep", true)
2675 .await
2676 .unwrap();
2677
2678 let block = mfs.get_block(&root).await.unwrap();
2679 let branch = parse_file_branch(block.data()).expect("root is a file branch");
2680 let child = mfs.get_block(&branch.links[0].cid).await.unwrap();
2681 assert!(
2682 parse_file_branch(child.data()).is_some(),
2683 "tree must be deeper than two levels to exercise the recursive editor"
2684 );
2685
2686 let patch = b"DEEP-OVERWRITE-PATCH";
2687 let off = 7 * CHUNK as usize + 100;
2688 mfs.write_with(
2689 "/deep",
2690 patch,
2691 WriteOptions {
2692 offset: off as u64,
2693 ..Default::default()
2694 },
2695 )
2696 .await
2697 .unwrap();
2698 let mut expected = content.clone();
2699 expected[off..off + patch.len()].copy_from_slice(patch);
2700 let (exp_root, _) = encode_file_bf(&expected, bf);
2701 assert_eq!(
2702 mfs.stat("/deep").await.unwrap().cid,
2703 exp_root,
2704 "overwrite preserves the deep structure canonically"
2705 );
2706 assert_eq!(mfs.read("/deep").await.unwrap(), expected);
2707
2708 let app = fill(50_000, 32);
2709 mfs.write_with(
2710 "/deep",
2711 &app,
2712 WriteOptions {
2713 offset: expected.len() as u64,
2714 ..Default::default()
2715 },
2716 )
2717 .await
2718 .unwrap();
2719 expected.extend_from_slice(&app);
2720 assert_canonical(&mfs, "/deep", &expected).await;
2721 }
2722
2723 #[tokio::test]
2724 async fn mv_into_dir_does_not_clobber_colliding_entry() {
2725 let mfs = mfs().await;
2726 mfs.write("/a/x/keep", b"src", true).await.unwrap();
2727 mfs.write("/b/x/old", b"victim", true).await.unwrap();
2728
2729 let err = mfs.mv("/a/x", "/b", false).await.unwrap_err();
2730 assert!(matches!(
2731 err.downcast_ref::<MfsError>(),
2732 Some(MfsError::AlreadyExists(_))
2733 ));
2734 assert_eq!(
2735 mfs.read("/b/x/old").await.unwrap(),
2736 b"victim",
2737 "destination subtree survives"
2738 );
2739 assert_eq!(
2740 mfs.read("/a/x/keep").await.unwrap(),
2741 b"src",
2742 "source preserved on failure"
2743 );
2744 }
2745
2746 #[tokio::test]
2747 async fn mv_missing_source_errors() {
2748 let mfs = mfs().await;
2749 mfs.mkdir("/d", false).await.unwrap();
2750 assert!(
2751 mfs.mv("/d/missing", "/d", false).await.is_err(),
2752 "no false success via no-op guard"
2753 );
2754 assert!(mfs.mv("/nope", "/nope", false).await.is_err());
2755 }
2756
2757 #[tokio::test]
2758 async fn rm_force_propagates_structural_errors() {
2759 let mfs = mfs().await;
2760 mfs.write("/a", b"file", true).await.unwrap();
2761 assert!(mfs.rm_force("/a/b", false).await.is_err());
2763 assert_eq!(mfs.read("/a").await.unwrap(), b"file");
2764 }
2765
2766 #[tokio::test]
2767 async fn root_cumulative_size_is_monotonic() {
2768 let mfs = mfs().await;
2769 let big = fill(600_000, 61);
2770 mfs.write("/dir/a", &big, true).await.unwrap();
2771 mfs.cp("/dir/a", "/dir/b", false).await.unwrap();
2772
2773 let root = mfs.stat("/").await.unwrap().cumulative_size;
2774 let dir = mfs.stat("/dir").await.unwrap().cumulative_size;
2775 assert!(
2776 root >= dir,
2777 "root cumulative {root} must be >= its child dir {dir}"
2778 );
2779 }
2780}