wasmer_wasix/syscalls/wasi/
path_rename.rs1use std::path::PathBuf;
2
3use super::*;
4use crate::syscalls::*;
5
6#[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 let source_inode = wasi_try_ok!(state.fs.get_inode_at_path(
81 inodes,
82 source_fd,
83 source_path,
84 true
85 ));
86 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 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 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 !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 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}