Skip to main content

wasmer_wasix/syscalls/wasi/
path_rename.rs

1use std::path::PathBuf;
2
3use super::*;
4use crate::syscalls::*;
5
6/// ### `path_rename()`
7/// Rename a file or directory
8/// Inputs:
9/// - `Fd old_fd`
10///     The base directory for `old_path`
11/// - `const char* old_path`
12///     Pointer to UTF8 bytes, the file to be renamed
13/// - `u32 old_path_len`
14///     The number of bytes to read from `old_path`
15/// - `Fd new_fd`
16///     The base directory for `new_path`
17/// - `const char* new_path`
18///     Pointer to UTF8 bytes, the new file name
19/// - `u32 new_path_len`
20///     The number of bytes to read from `new_path`
21#[instrument(level = "trace", skip_all, fields(%old_fd, %new_fd, old_path = field::Empty, new_path = field::Empty), ret)]
22pub fn path_rename<M: MemorySize>(
23    mut ctx: FunctionEnvMut<'_, WasiEnv>,
24    old_fd: WasiFd,
25    old_path: WasmPtr<u8, M>,
26    old_path_len: M::Offset,
27    new_fd: WasiFd,
28    new_path: WasmPtr<u8, M>,
29    new_path_len: M::Offset,
30) -> Result<Errno, WasiError> {
31    WasiEnv::do_pending_operations(&mut ctx)?;
32
33    let env = ctx.data();
34    let (memory, mut state, inodes) = unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
35    let source_str = unsafe { get_input_str_ok!(&memory, old_path, old_path_len) };
36    Span::current().record("old_path", source_str.as_str());
37    let target_str = unsafe { get_input_str_ok!(&memory, new_path, new_path_len) };
38    Span::current().record("new_path", target_str.as_str());
39
40    let ret = path_rename_internal(&mut ctx, old_fd, &source_str, new_fd, &target_str)?;
41    let env = ctx.data();
42
43    if ret == Errno::Success {
44        #[cfg(feature = "journal")]
45        if env.enable_journal {
46            JournalEffector::save_path_rename(&mut ctx, old_fd, source_str, new_fd, target_str)
47                .map_err(|err| {
48                    tracing::error!("failed to save path rename event - {}", err);
49                    WasiError::Exit(ExitCode::from(Errno::Fault))
50                })?;
51        }
52    }
53    Ok(ret)
54}
55
56pub fn path_rename_internal(
57    ctx: &mut FunctionEnvMut<'_, WasiEnv>,
58    source_fd: WasiFd,
59    source_path: &str,
60    target_fd: WasiFd,
61    target_path: &str,
62) -> Result<Errno, WasiError> {
63    let env = ctx.data();
64    let (memory, mut state, inodes) = unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
65
66    let mut moved_ephemeral_symlink = false;
67
68    {
69        let source_fd = wasi_try_ok!(state.fs.get_fd(source_fd));
70        if !source_fd.inner.rights.contains(Rights::PATH_RENAME_SOURCE) {
71            return Ok(Errno::Access);
72        }
73        let target_fd = wasi_try_ok!(state.fs.get_fd(target_fd));
74        if !target_fd.inner.rights.contains(Rights::PATH_RENAME_TARGET) {
75            return Ok(Errno::Access);
76        }
77    }
78
79    // this is to be sure the source file is fetched from the filesystem if needed
80    let source_inode = wasi_try_ok!(state.fs.get_inode_at_path(
81        inodes,
82        source_fd,
83        source_path,
84        true
85    ));
86    // Create the destination inode if the file exists.
87    let _ = state
88        .fs
89        .get_inode_at_path(inodes, target_fd, target_path, true);
90    let (source_parent_inode, source_entry_name) = wasi_try_ok!(state.fs.get_parent_inode_at_path(
91        inodes,
92        source_fd,
93        Path::new(source_path),
94        true
95    ));
96    let (target_parent_inode, target_entry_name) = wasi_try_ok!(state.fs.get_parent_inode_at_path(
97        inodes,
98        target_fd,
99        Path::new(target_path),
100        true
101    ));
102    let source_guest_path = {
103        let guard = source_parent_inode.read();
104        match guard.deref() {
105            Kind::Dir { path, .. } => crate::fs::PosixPath::from_path(path)
106                .join(&crate::fs::PosixPath::new(&source_entry_name))
107                .into_path_buf(),
108            Kind::Root { .. } => return Ok(Errno::Notcapable),
109            Kind::Socket { .. }
110            | Kind::PipeTx { .. }
111            | Kind::PipeRx { .. }
112            | Kind::DuplexPipe { .. }
113            | Kind::EventNotifications { .. }
114            | Kind::Epoll { .. } => return Ok(Errno::Inval),
115            Kind::Symlink { .. } | Kind::File { .. } | Kind::Buffer { .. } => {
116                debug!("fatal internal logic error: parent of inode is not a directory");
117                return Ok(Errno::Inval);
118            }
119        }
120    };
121    let mut need_create = true;
122    let target_guest_path = {
123        let guard = target_parent_inode.read();
124        match guard.deref() {
125            Kind::Dir { entries, path, .. } => {
126                if entries.contains_key(&target_entry_name) {
127                    need_create = false;
128                }
129                crate::fs::PosixPath::from_path(path)
130                    .join(&crate::fs::PosixPath::new(&target_entry_name))
131                    .into_path_buf()
132            }
133            Kind::Root { .. } => return Ok(Errno::Notcapable),
134            Kind::Socket { .. }
135            | Kind::PipeTx { .. }
136            | Kind::PipeRx { .. }
137            | Kind::DuplexPipe { .. }
138            | Kind::EventNotifications { .. }
139            | Kind::Epoll { .. } => return Ok(Errno::Inval),
140            Kind::Symlink { .. } | Kind::File { .. } | Kind::Buffer { .. } => {
141                debug!("fatal internal logic error: parent of inode is not a directory");
142                return Ok(Errno::Inval);
143            }
144        }
145    };
146
147    if source_parent_inode.ino() == target_parent_inode.ino()
148        && source_entry_name == target_entry_name
149    {
150        return Ok(Errno::Success);
151    }
152
153    let source_is_dir = {
154        let guard = source_inode.read();
155        matches!(guard.deref(), Kind::Dir { .. })
156    };
157    if source_is_dir
158        && crate::fs::PosixPath::from_path(&target_guest_path)
159            .strip_prefix(&crate::fs::PosixPath::from_path(&source_guest_path))
160            .is_some()
161    {
162        return Ok(Errno::Inval);
163    }
164
165    let source_entry = {
166        let mut guard = source_parent_inode.write();
167        match guard.deref_mut() {
168            Kind::Dir { entries, .. } => {
169                wasi_try_ok!(entries.remove(&source_entry_name).ok_or(Errno::Noent))
170            }
171            Kind::Root { .. } => return Ok(Errno::Notcapable),
172            Kind::Socket { .. }
173            | Kind::PipeRx { .. }
174            | Kind::PipeTx { .. }
175            | Kind::DuplexPipe { .. }
176            | Kind::EventNotifications { .. }
177            | Kind::Epoll { .. } => {
178                return Ok(Errno::Inval);
179            }
180            Kind::Symlink { .. } | Kind::File { .. } | Kind::Buffer { .. } => {
181                debug!("fatal internal logic error: parent of inode is not a directory");
182                return Ok(Errno::Inval);
183            }
184        }
185    };
186
187    {
188        let mut guard = source_entry.write();
189        match guard.deref_mut() {
190            Kind::File { path, .. } => {
191                let result = {
192                    let path_clone = path.clone();
193                    drop(guard);
194                    let state = state;
195                    let target_guest_path = target_guest_path.clone();
196                    __asyncify_light(env, None, async move {
197                        state.fs_rename(path_clone, &target_guest_path).await
198                    })?
199                };
200                // if the above operation failed we have to revert the previous change and then fail
201                if let Err(e) = result {
202                    let mut guard = source_parent_inode.write();
203                    if let Kind::Dir { entries, .. } = guard.deref_mut() {
204                        entries.insert(source_entry_name, source_entry);
205                        return Ok(e);
206                    }
207                } else {
208                    let mut guard = source_entry.write();
209                    if let Kind::File { path, .. } = guard.deref_mut() {
210                        *path = target_guest_path.clone();
211                    } else {
212                        unreachable!()
213                    }
214                }
215            }
216            Kind::Dir { path, .. } => {
217                let cloned_path = path.clone();
218                let res = {
219                    let state = state;
220                    let target_guest_path = target_guest_path.clone();
221                    __asyncify_light(env, None, async move {
222                        state.fs_rename(cloned_path, &target_guest_path).await
223                    })?
224                };
225                if let Err(e) = res {
226                    return Ok(e);
227                }
228                {
229                    let source_dir_path = path.clone();
230                    drop(guard);
231                    rename_inode_tree(&source_entry, &source_dir_path, &target_guest_path);
232                }
233            }
234            Kind::Symlink {
235                path_to_symlink,
236                relative_path,
237                ..
238            } => {
239                let is_ephemeral = state
240                    .fs
241                    .ephemeral_symlink_at(source_guest_path.as_path())
242                    .is_some();
243                let res = {
244                    let state = state;
245                    let from = source_guest_path.clone();
246                    let to = target_guest_path.clone();
247                    __asyncify_light(env, None, async move { state.fs_rename(from, to).await })?
248                };
249                match (res, is_ephemeral) {
250                    (Ok(()), _) | (Err(Errno::Noent), true) => {}
251                    (Err(e), _) => {
252                        let mut guard = source_parent_inode.write();
253                        if let Kind::Dir { entries, .. } = guard.deref_mut() {
254                            entries.insert(source_entry_name, source_entry.clone());
255                            return Ok(e);
256                        }
257                    }
258                }
259
260                let new_path_to_symlink = state
261                    .fs
262                    .rebase_symlink_location(target_guest_path.as_path());
263                *path_to_symlink = new_path_to_symlink.clone();
264                if is_ephemeral {
265                    state.fs.move_ephemeral_symlink(
266                        source_guest_path.as_path(),
267                        target_guest_path.as_path(),
268                        new_path_to_symlink,
269                        relative_path.clone(),
270                    );
271                    moved_ephemeral_symlink = true;
272                }
273            }
274            Kind::Buffer { .. }
275            | Kind::Socket { .. }
276            | Kind::PipeTx { .. }
277            | Kind::PipeRx { .. }
278            | Kind::DuplexPipe { .. }
279            | Kind::Epoll { .. }
280            | Kind::EventNotifications { .. } => {}
281            Kind::Root { .. } => unreachable!("The root can not be moved"),
282        }
283    }
284
285    let source_size = source_entry.stat.read().unwrap().st_size;
286
287    if need_create {
288        let mut guard = target_parent_inode.write();
289        if let Kind::Dir { entries, .. } = guard.deref_mut() {
290            let result = entries.insert(target_entry_name.clone(), source_entry);
291            assert!(
292                result.is_none(),
293                "fatal error: race condition on filesystem detected or internal logic error"
294            );
295        }
296    }
297
298    // The target entry is created, one way or the other
299    let target_inode = state
300        .fs
301        .get_inode_at_path(inodes, target_fd, target_path, true)
302        .expect("Expected target inode to exist, and it's too late to safely fail");
303    *target_inode.name.write().unwrap() = target_entry_name.into();
304    target_inode.stat.write().unwrap().st_size = source_size;
305
306    // If the rename replaced an existing destination entry, clear any stale
307    // ephemeral symlink mapping for that path.
308    if !moved_ephemeral_symlink {
309        state
310            .fs
311            .unregister_ephemeral_symlink(target_guest_path.as_path());
312    }
313
314    Ok(Errno::Success)
315}
316
317fn rename_inode_tree(inode: &InodeGuard, source_dir_path: &Path, target_dir_path: &Path) {
318    let children;
319
320    let mut guard = inode.write();
321    match guard.deref_mut() {
322        Kind::File { path, .. } => {
323            *path = adjust_path(path, source_dir_path, target_dir_path);
324            return;
325        }
326        Kind::Dir { path, entries, .. } => {
327            *path = adjust_path(path, source_dir_path, target_dir_path);
328            children = entries.values().cloned().collect::<Vec<_>>();
329        }
330        _ => return,
331    }
332    drop(guard);
333
334    for child in children {
335        rename_inode_tree(&child, source_dir_path, target_dir_path);
336    }
337}
338
339fn adjust_path(path: &Path, source_dir_path: &Path, target_dir_path: &Path) -> PathBuf {
340    let path = crate::fs::PosixPath::from_path(path);
341    let source_dir_path = crate::fs::PosixPath::from_path(source_dir_path);
342    let Some(relative_path) = path.strip_prefix(&source_dir_path) else {
343        // A directory entry created by path_link may retain the backing path
344        // of its source when the mounted filesystem cannot materialize hard
345        // links. Moving its parent must move the entry, not that shared
346        // backing file.
347        return PathBuf::from(path.as_str());
348    };
349    crate::fs::PosixPath::from_path(target_dir_path)
350        .join(&relative_path)
351        .into_path_buf()
352}
353
354#[cfg(test)]
355mod tests {
356    use super::adjust_path;
357    use std::path::Path;
358
359    #[test]
360    fn moving_a_directory_preserves_external_hard_link_backing_paths() {
361        let adjusted = adjust_path(
362            Path::new("/store/files/content"),
363            Path::new("/workspace/package_tmp"),
364            Path::new("/workspace/package"),
365        );
366
367        assert_eq!(adjusted, Path::new("/store/files/content"));
368    }
369
370    #[test]
371    fn moving_a_directory_rebases_ordinary_descendants() {
372        let adjusted = adjust_path(
373            Path::new("/workspace/package_tmp/lib/index.js"),
374            Path::new("/workspace/package_tmp"),
375            Path::new("/workspace/package"),
376        );
377
378        assert_eq!(adjusted, Path::new("/workspace/package/lib/index.js"));
379    }
380}