cargo_toml/
afs.rs

1use crate::{Error, Manifest, Value};
2use std::collections::HashSet;
3use std::fs::read_dir;
4use std::io;
5use std::path::{Path, PathBuf};
6
7/// This crate supports reading `Cargo.toml` not only from a real directory, but also directly from other sources, like tarballs or bare git repos (BYO directory reader).
8///
9/// The implementation must have a concept of the current directory, which is set to the crate's manifest dir.
10pub trait AbstractFilesystem {
11    /// List all files and directories at the given relative path (no leading `/`).
12    fn file_names_in(&self, rel_path: &str) -> io::Result<HashSet<Box<str>>>;
13
14    /// `parse_root_workspace` is preferred.
15    ///
16    /// The `rel_path_hint` may be specified explicitly by `package.workspace` (it may be relative like `"../"`, without `Cargo.toml`) or `None`,
17    /// which means you have to search for workspace's `Cargo.toml` in parent directories.
18    ///
19    /// Read bytes of the root workspace manifest TOML file and return the path it's been read from.
20    /// The path needs to be an absolute path, because it will be used as the base path for inherited readmes, and would be ambiguous otherwise.
21    #[deprecated(note = "implement parse_root_workspace instead")]
22    #[doc(hidden)]
23    fn read_root_workspace(&self, _rel_path_hint: Option<&Path>) -> io::Result<(Vec<u8>, PathBuf)> {
24        Err(io::Error::new(io::ErrorKind::Unsupported, "AbstractFilesystem::read_root_workspace unimplemented"))
25    }
26
27    /// The `rel_path_hint` may be specified explicitly by `package.workspace` (it may be relative like `"../"`, without `Cargo.toml`) or `None`,
28    /// which means you have to search for workspace's `Cargo.toml` in parent directories.
29    ///
30    /// Read and parse the root workspace manifest TOML file and return the path it's been read from.
31    /// The path needs to be an absolute path, because it will be used as the base path for inherited readmes, and would be ambiguous otherwise.
32    #[allow(deprecated)]
33    fn parse_root_workspace(&self, rel_path_hint: Option<&Path>) -> Result<(Manifest<Value>, PathBuf), Error> {
34        let (data, path) = self.read_root_workspace(rel_path_hint).map_err(|e| Error::Workspace(Box::new((e.into(), rel_path_hint.map(PathBuf::from)))))?;
35        let manifest = match Manifest::from_slice(&data) {
36            Ok(m) => m,
37            Err(e) => return Err(Error::Workspace(Box::new((e, Some(path))))),
38        };
39        if manifest.workspace.is_none() {
40            return Err(Error::Workspace(Box::new(
41                (Error::WorkspaceIntegrity("Not a Workspace.\nUse package.workspace to select a differnt path, or implement cargo_toml::AbstractFilesystem::parse_root_workspace".into()), Some(path))
42            )));
43        }
44        Ok((manifest, path))
45    }
46}
47
48impl<T> AbstractFilesystem for &T
49where
50    T: AbstractFilesystem + ?Sized,
51{
52    fn file_names_in(&self, rel_path: &str) -> io::Result<HashSet<Box<str>>> {
53        <T as AbstractFilesystem>::file_names_in(*self, rel_path)
54    }
55
56    #[allow(deprecated)]
57    fn read_root_workspace(&self, rel_path_hint: Option<&Path>) -> io::Result<(Vec<u8>, PathBuf)> {
58        <T as AbstractFilesystem>::read_root_workspace(*self, rel_path_hint)
59    }
60
61    fn parse_root_workspace(&self, rel_path_hint: Option<&Path>) -> Result<(Manifest<Value>, PathBuf), Error> {
62        <T as AbstractFilesystem>::parse_root_workspace(*self, rel_path_hint)
63    }
64}
65
66/// [`AbstractFilesystem`] implementation for real files.
67pub struct Filesystem<'a> {
68    path: &'a Path,
69}
70
71impl<'a> Filesystem<'a> {
72    #[must_use]
73    pub fn new(path: &'a Path) -> Self {
74        Self { path }
75    }
76}
77
78impl<'a> AbstractFilesystem for Filesystem<'a> {
79    fn file_names_in(&self, rel_path: &str) -> io::Result<HashSet<Box<str>>> {
80        Ok(read_dir(self.path.join(rel_path))?.filter_map(|entry| {
81            entry.ok().map(|e| {
82                e.file_name().to_string_lossy().into_owned().into()
83            })
84        })
85        .collect())
86    }
87
88    fn parse_root_workspace(&self, path: Option<&Path>) -> Result<(Manifest<Value>, PathBuf), Error> {
89        match path {
90            Some(path) => {
91                let ws = self.path.join(path);
92                let toml_path = ws.join("Cargo.toml");
93                let data = match std::fs::read(&toml_path) {
94                    Ok(d) => d,
95                    Err(e) => return Err(Error::Workspace(Box::new((Error::Io(e), Some(toml_path))))),
96                };
97                Ok((parse_workspace(&data, &toml_path)?, ws))
98            },
99            None => {
100                // Try relative path first
101                match find_workspace(self.path) {
102                    Ok(found) => Ok(found),
103                    Err(err) if self.path.is_absolute() => Err(err),
104                    Err(_) => find_workspace(&self.path.ancestors().last().unwrap().canonicalize()?),
105                }
106            },
107        }
108    }
109}
110
111#[inline(never)]
112fn find_workspace(path: &Path) -> Result<(Manifest<Value>, PathBuf), Error> {
113    if path.parent().is_none() {
114        return Err(io::Error::new(io::ErrorKind::NotFound, format!("Can't find workspace in '{}', because it has no parent directories", path.display())).into())
115    }
116    let mut last_error = None;
117    path.ancestors().skip(1)
118        .map(|parent| parent.join("Cargo.toml"))
119        .find_map(|p| {
120            let data = std::fs::read(&p).ok()?;
121            match parse_workspace(&data, &p) {
122                Ok(manifest) => Some((manifest, p)),
123                Err(e) => {
124                    last_error = Some(e);
125                    None
126                },
127            }
128        })
129        .ok_or(last_error.unwrap_or_else(|| {
130            let has_slash = path.to_str().is_some_and(|s| s.ends_with('/'));
131            io::Error::new(io::ErrorKind::NotFound, format!("Can't find workspace in '{}{}..'", path.display(), if has_slash {""} else {"/"})).into()
132        }))
133}
134
135#[inline(never)]
136fn parse_workspace(data: &[u8], path: &Path) -> Result<Manifest<Value>, Error> {
137    let manifest = Manifest::from_slice(data)?;
138    if manifest.workspace.is_none() {
139        return Err(Error::WorkspaceIntegrity(format!("Manifest at {} was expected to be a workspace.", path.display())));
140    }
141    Ok(manifest)
142}