char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
9.7 KiB255 linesraw
1use super::paths::{resolve, tree};
2use super::*;
3
4// Link authorization and rebasing must not race another managed link mutation.
5pub(super) static MUTATIONS: Mutex<()> = Mutex::new(());
6
7pub(super) fn authorize_target(
8    caps: &Capabilities,
9    link: &PathTarget,
10    text: &Path,
11    label: &str,
12) -> io::Result<()> {
13    if caps.unrestricted_read() && caps.unrestricted_write() {
14        return Ok(());
15    }
16    let denied_target = || {
17        io::Error::new(
18            io::ErrorKind::PermissionDenied,
19            format!("symlink target for {label}"),
20        )
21    };
22    let path = link
23        .as_path()
24        .parent()
25        .ok_or_else(denied_target)?
26        .join(text);
27    let target = PathTarget::for_creation(path).map_err(|_| denied_target())?;
28    tree(caps, &target, label, false).map_err(|_| denied_target())?;
29    tree(caps, &target, label, true).map_err(|_| denied_target())?;
30
31    let mut root = PathBuf::new();
32    let mut names = Vec::new();
33    for component in target.as_path().components() {
34        match component {
35            Component::Prefix(_) | Component::RootDir => root.push(component.as_os_str()),
36            Component::Normal(name) => names.push(name),
37            _ => return Err(denied_target()),
38        }
39    }
40    let mut directory =
41        Dir::open_ambient_dir(root, cap_std::ambient_authority()).map_err(|_| denied_target())?;
42    for (index, name) in names.iter().enumerate() {
43        match directory.symlink_metadata(name) {
44            Ok(metadata) if metadata.is_symlink() => return Err(denied_target()),
45            Ok(_) if index + 1 == names.len() => return Ok(()),
46            Ok(_) => {
47                directory = directory
48                    .open_dir_nofollow(name)
49                    .map_err(|_| denied_target())?;
50            }
51            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
52            Err(_) => return Err(denied_target()),
53        }
54    }
55    Ok(())
56}
57
58fn validate(target: &Path, kind: Option<&str>) -> io::Result<()> {
59    if target.as_os_str().is_empty()
60        || target.as_os_str().as_encoded_bytes().contains(&0)
61        || kind.is_some_and(|kind| kind != "file" && kind != "dir")
62    {
63        return Err(io::Error::new(
64            io::ErrorKind::InvalidInput,
65            "Invalid symbolic link target or type",
66        ));
67    }
68    Ok(())
69}
70
71pub(super) fn create(directory: &Dir, name: &std::ffi::OsStr, target: &Path) -> io::Result<()> {
72    #[cfg(not(windows))]
73    {
74        directory.symlink_contents(target, name)
75    }
76    #[cfg(windows)]
77    {
78        let _ = (directory, name);
79        Err(io::Error::new(
80            io::ErrorKind::Unsupported,
81            "not yet implemented: capability-safe Windows symlink creation",
82        ))
83    }
84}
85
86pub(super) fn contains_links(directory: Dir) -> io::Result<bool> {
87    use cap_fs_ext::MetadataExt;
88    let metadata = directory.dir_metadata()?;
89    let mut seen = std::collections::HashSet::from([(metadata.dev(), metadata.ino())]);
90    let mut stack = vec![(directory.entries()?, directory)];
91    while let Some((entries, directory)) = stack.last_mut() {
92        let Some(entry) = entries.next() else {
93            stack.pop();
94            continue;
95        };
96        let entry = entry?;
97        let kind = entry.file_type()?;
98        if kind.is_symlink() {
99            return Ok(true);
100        }
101        #[cfg(windows)]
102        {
103            use cap_std::fs::MetadataExt;
104            if directory
105                .symlink_metadata(entry.file_name())?
106                .file_attributes()
107                & 0x400
108                != 0
109            {
110                return Ok(true);
111            }
112        }
113        if kind.is_dir() {
114            let child = directory.open_dir_nofollow(entry.file_name())?;
115            let metadata = child.dir_metadata()?;
116            if seen.insert((metadata.dev(), metadata.ino())) {
117                stack.push((child.entries()?, child));
118            }
119        }
120    }
121    Ok(false)
122}
123
124pub(super) fn text(path: PathBuf) -> io::Result<String> {
125    path.into_os_string().into_string().map_err(|_| {
126        io::Error::new(
127            io::ErrorKind::Unsupported,
128            "not yet implemented: non-UTF-8 link paths",
129        )
130    })
131}
132
133pub(super) fn ops(files: Arc<Files>) -> Vec<Op> {
134    let ops = files.clone();
135    let symlink = files.clone();
136    let read_link = files.clone();
137    let copy_link = files.clone();
138    vec![
139        ops.io_ops(
140            "op_file_copy_link",
141            "op_file_copy_link_sync",
142            move |(source, destination): (String, String)| {
143                let caps = copy_link.caps.clone();
144                let pending = copy_link.pool.run(move || {
145                    let _guard = MUTATIONS
146                        .lock()
147                        .unwrap_or_else(std::sync::PoisonError::into_inner);
148                    let from = resolve(&caps, &source, true, false)?;
149                    let to = resolve(&caps, &destination, true, true)?;
150                    let (from_dir, from_name) = parent(from.as_path())?;
151                    let metadata = from_dir.symlink_metadata(&from_name)?;
152                    if !metadata.is_symlink() {
153                        return Err(io::Error::new(
154                            io::ErrorKind::InvalidInput,
155                            "Source is not a symbolic link",
156                        ));
157                    }
158                    let contents = from_dir.read_link_contents(from_name)?;
159                    validate(&contents, None)?;
160                    #[cfg(windows)]
161                    {
162                        let _ = (to, contents);
163                        return Err::<bool, _>(io::Error::new(
164                            io::ErrorKind::Unsupported,
165                            "not yet implemented: capability-safe Windows symlink creation",
166                        ));
167                    }
168                    #[cfg(not(windows))]
169                    {
170                        use cap_fs_ext::MetadataExt;
171                        let (to_dir, to_name) = super::paths::mutable_entry(&to)?;
172                        authorize_target(&caps, &to, &contents, &destination)?;
173                        match to_dir.symlink_metadata(&to_name) {
174                            Ok(existing)
175                                if existing.dev() == metadata.dev()
176                                    && existing.ino() == metadata.ino() =>
177                            {
178                                return Err(io::Error::new(
179                                    io::ErrorKind::InvalidInput,
180                                    "Source and destination are the same link",
181                                ));
182                            }
183                            Ok(existing) if existing.is_dir() => {
184                                return Err(io::Error::new(
185                                    io::ErrorKind::IsADirectory,
186                                    "Link destination is a directory",
187                                ));
188                            }
189                            Ok(_) => to_dir.remove_file_or_symlink(&to_name)?,
190                            Err(error) if error.kind() == io::ErrorKind::NotFound => (),
191                            Err(error) => return Err(error),
192                        }
193                        create(&to_dir, &to_name, &contents)?;
194                        Ok(true)
195                    }
196                });
197                async move { Ok::<_, Infallible>(receive(pending).await) }
198            },
199        ),
200        ops.io_ops(
201            "op_file_symlink",
202            "op_file_symlink_sync",
203            move |(target, link, kind): (String, String, Option<String>)| {
204                let caps = symlink.caps.clone();
205                let pending = symlink.pool.run(move || {
206                    validate(Path::new(&target), kind.as_deref())?;
207                    let _guard = MUTATIONS
208                        .lock()
209                        .unwrap_or_else(std::sync::PoisonError::into_inner);
210                    let entry = resolve(&caps, &link, true, true)?;
211                    let (directory, name) = super::paths::mutable_entry(&entry)?;
212                    authorize_target(&caps, &entry, Path::new(&target), &link)?;
213                    create(&directory, &name, Path::new(&target))?;
214                    Ok(true)
215                });
216                async move { Ok::<_, Infallible>(receive(pending).await) }
217            },
218        ),
219        ops.io_ops(
220            "op_file_read_link",
221            "op_file_read_link_sync",
222            move |path: String| {
223                let caps = read_link.caps.clone();
224                let pending = read_link.pool.run(move || {
225                    let entry = resolve(&caps, &path, true, false)?;
226                    let (directory, name) = parent(entry.as_path())?;
227                    text(directory.read_link_contents(name)?)
228                });
229                async move { Ok::<_, Infallible>(receive(pending).await) }
230            },
231        ),
232        ops.io_ops(
233            "op_file_real_path",
234            "op_file_real_path_sync",
235            move |path: String| {
236                let caps = files.caps.clone();
237                let pending = files.pool.run(move || {
238                    let target = resolve(&caps, &path, false, false)?;
239                    let (directory, name) = parent(target.as_path())?;
240                    if directory.symlink_metadata(name)?.is_symlink() {
241                        return Err(io::Error::new(
242                            io::ErrorKind::NotFound,
243                            "Symbolic link changed or has a missing target",
244                        ));
245                    }
246                    text(target.into_path())
247                });
248                async move { Ok::<_, Infallible>(receive(pending).await) }
249            },
250        ),
251    ]
252    .into_iter()
253    .flatten()
254    .collect()
255}