1 | // SPDX-License-Identifier: GPL-3.0-or-later |
2 | |
3 | #include "mos/filesystem/dentry.hpp" |
4 | |
5 | #include "mos/assert.hpp" |
6 | #include "mos/filesystem/inode.hpp" |
7 | #include "mos/filesystem/mount.hpp" |
8 | #include "mos/filesystem/vfs.hpp" |
9 | #include "mos/filesystem/vfs_types.hpp" |
10 | #include "mos/filesystem/vfs_utils.hpp" |
11 | #include "mos/io/io.hpp" |
12 | #include "mos/lib/sync/spinlock.hpp" |
13 | #include "mos/syslog/printk.hpp" |
14 | #include "mos/tasks/process.hpp" |
15 | #include "mos/tasks/task_types.hpp" |
16 | |
17 | #include <atomic> |
18 | #include <mos/filesystem/fs_types.h> |
19 | #include <mos/lib/structures/hashmap_common.hpp> |
20 | #include <mos_stdio.hpp> |
21 | #include <mos_stdlib.hpp> |
22 | #include <mos_string.hpp> |
23 | |
24 | // A path in its string form is composed of "segments" separated |
25 | // by a slash "/", a path may: |
26 | // |
27 | // - begin with a slash, indicating that's an absolute path |
28 | // - begin without a slash, indicating that's a relative path |
29 | // (relative to the current working directory (AT_FDCWD)) |
30 | // |
31 | // A path may end with a slash, indicating that the caller expects |
32 | // the path to be a directory |
33 | |
34 | // The two functions below have circular dependencies, so we need to forward declare them |
35 | // Both of them return a referenced dentry, no need to refcount them again |
36 | static PtrResult<dentry_t> dentry_resolve_lastseg(dentry_t *parent, char *leaf, lastseg_resolve_flags_t flags, bool *symlink_resolved); |
37 | static PtrResult<dentry_t> dentry_resolve_follow_symlink(dentry_t *dentry, lastseg_resolve_flags_t flags); |
38 | |
39 | /** |
40 | * @brief Lookup the parent directory of a given path, and return the last segment of the path in last_seg_out |
41 | * |
42 | * @param base_dir A directory to start the lookup from |
43 | * @param root_dir The root directory of the filesystem, the lookup will not go above this directory |
44 | * @param original_path The path to lookup |
45 | * @param last_seg_out The last segment of the path will be returned in this parameter, the caller is responsible for freeing it |
46 | * @return dentry_t* The parent directory of the path, or NULL if the path is invalid, the dentry will be referenced |
47 | */ |
48 | static PtrResult<dentry_t> dentry_resolve_to_parent(dentry_t *base_dir, dentry_t *root_dir, const char *original_path, char **last_seg_out) |
49 | { |
50 | pr_dinfo2(dcache, "lookup parent of '%s'" , original_path); |
51 | MOS_ASSERT_X(base_dir && root_dir && original_path, "Invalid VFS lookup parameters" ); |
52 | if (last_seg_out != NULL) |
53 | *last_seg_out = NULL; |
54 | |
55 | dentry_t *parent_ref = [&]() |
56 | { |
57 | dentry_t *tmp = path_is_absolute(path: original_path) ? root_dir : base_dir; |
58 | if (tmp->is_mountpoint) |
59 | tmp = dentry_get_mount(dentry: tmp)->root; // if it's a mountpoint, jump to mounted filesystem |
60 | return dentry_ref_up_to(dentry: tmp, root: root_dir); |
61 | }(); |
62 | |
63 | char *saveptr = NULL; |
64 | char *path = strdup(src: original_path); |
65 | const char *current_seg = strtok_r(str: path, PATH_DELIM_STR, saveptr: &saveptr); |
66 | if (unlikely(current_seg == NULL)) |
67 | { |
68 | // this only happens if the path is empty, or contains only slashes |
69 | // in which case we return the base directory |
70 | kfree(ptr: path); |
71 | if (last_seg_out != NULL) |
72 | *last_seg_out = NULL; |
73 | return parent_ref; |
74 | } |
75 | |
76 | while (true) |
77 | { |
78 | pr_dinfo2(dcache, "lookup parent: current segment '%s'" , current_seg); |
79 | const char *const next = strtok_r(NULL, PATH_DELIM_STR, saveptr: &saveptr); |
80 | if (parent_ref->inode->type == FILE_TYPE_SYMLINK) |
81 | { |
82 | // this is the real interesting dir |
83 | auto parent_real_ref = dentry_resolve_follow_symlink(dentry: parent_ref, flags: RESOLVE_EXPECT_EXIST | RESOLVE_EXPECT_DIR); |
84 | dentry_unref(dentry: parent_ref); |
85 | if (parent_real_ref.isErr()) |
86 | return -ENOENT; // the symlink target does not exist |
87 | parent_ref = parent_real_ref.get(); |
88 | } |
89 | |
90 | if (next == NULL) |
91 | { |
92 | // "current_seg" is the last segment of the path |
93 | if (last_seg_out != NULL) |
94 | { |
95 | const bool ends_with_slash = original_path[strlen(str: original_path) - 1] == PATH_DELIM; |
96 | char *tmp = kcalloc<char>(n_members: strlen(str: current_seg) + 2); // +2 for the null terminator and the slash |
97 | strcpy(dest: tmp, src: current_seg); |
98 | if (ends_with_slash) |
99 | strcat(dest: tmp, PATH_DELIM_STR); |
100 | *last_seg_out = tmp; |
101 | } |
102 | |
103 | kfree(ptr: path); |
104 | return parent_ref; |
105 | } |
106 | |
107 | if (strncmp(str1: current_seg, str2: "." , n: 2) == 0 || strcmp(str1: current_seg, str2: "./" ) == 0) |
108 | { |
109 | current_seg = next; |
110 | continue; |
111 | } |
112 | |
113 | if (strncmp(str1: current_seg, str2: ".." , n: 3) == 0 || strcmp(str1: current_seg, str2: "../" ) == 0) |
114 | { |
115 | if (parent_ref == root_dir) |
116 | { |
117 | // we can't go above the root directory |
118 | current_seg = next; |
119 | continue; |
120 | } |
121 | |
122 | dentry_t *parent = dentry_parent(dentry: *parent_ref); |
123 | |
124 | // don't recurse up to the root |
125 | MOS_ASSERT(dentry_unref_one_norelease(parent_ref)); |
126 | parent_ref = parent; |
127 | |
128 | // if the parent is a mountpoint, we need to jump to the mountpoint's parent |
129 | // and then jump to the mountpoint's parent's parent |
130 | // already referenced when we jumped to the mountpoint |
131 | if (parent_ref->is_mountpoint) |
132 | parent_ref = dentry_root_get_mountpoint(dentry: parent); |
133 | |
134 | current_seg = next; |
135 | continue; |
136 | } |
137 | |
138 | auto child_ref = dentry_lookup_child(parent: parent_ref, name: current_seg); |
139 | if (child_ref->inode == NULL) |
140 | { |
141 | *last_seg_out = NULL; |
142 | kfree(ptr: path); |
143 | dentry_try_release(dentry: child_ref.get()); |
144 | dentry_unref(dentry: parent_ref); |
145 | return -ENOENT; |
146 | } |
147 | |
148 | if (child_ref->is_mountpoint) |
149 | { |
150 | pr_dinfo2(dcache, "jumping to mountpoint %s" , child_ref->name.c_str()); |
151 | parent_ref = dentry_get_mount(dentry: child_ref.get())->root; // if it's a mountpoint, jump to the tree of mounted filesystem instead |
152 | |
153 | // refcount the mounted filesystem root |
154 | dentry_ref(dentry: parent_ref); |
155 | } |
156 | else |
157 | { |
158 | parent_ref = child_ref.get(); |
159 | } |
160 | |
161 | current_seg = next; |
162 | } |
163 | |
164 | MOS_UNREACHABLE(); |
165 | } |
166 | |
167 | static PtrResult<dentry_t> dentry_resolve_follow_symlink(dentry_t *d, lastseg_resolve_flags_t flags) |
168 | { |
169 | MOS_ASSERT_X(d != NULL && d->inode != NULL, "check before calling this function!" ); |
170 | MOS_ASSERT_X(d->inode->type == FILE_TYPE_SYMLINK, "check before calling this function!" ); |
171 | |
172 | if (!d->inode->ops || !d->inode->ops->readlink) |
173 | mos_panic("inode does not support readlink (symlink) operation, but it's a symlink!" ); |
174 | |
175 | const auto target = (char *) kcalloc<char>(MOS_PATH_MAX_LENGTH); |
176 | const size_t read = d->inode->ops->readlink(d, target, MOS_PATH_MAX_LENGTH); |
177 | if (read == 0) |
178 | { |
179 | mos_warn("symlink is empty" ); |
180 | return -ENOENT; // symlink is empty |
181 | } |
182 | |
183 | if (read == MOS_PATH_MAX_LENGTH) |
184 | { |
185 | mos_warn("symlink is too long" ); |
186 | return -ENAMETOOLONG; // symlink is too long |
187 | } |
188 | |
189 | target[read] = '\0'; // ensure null termination |
190 | |
191 | pr_dinfo2(dcache, "symlink target: %s" , target); |
192 | |
193 | char *last_segment = NULL; |
194 | auto parent_ref = dentry_resolve_to_parent(base_dir: dentry_parent(dentry: *d), root_dir: root_dentry, original_path: target, last_seg_out: &last_segment); |
195 | kfree(ptr: target); |
196 | if (parent_ref.isErr()) |
197 | return parent_ref; // the symlink target does not exist |
198 | |
199 | // it's possibly that the symlink target is also a symlink, this will be handled recursively |
200 | bool is_symlink = false; |
201 | const auto child_ref = dentry_resolve_lastseg(parent: parent_ref.get(), leaf: last_segment, flags, symlink_resolved: &is_symlink); |
202 | kfree(ptr: last_segment); |
203 | |
204 | // if symlink is true, we need to unref the parent_ref dentry as it's irrelevant now |
205 | if (child_ref.isErr() || is_symlink) |
206 | dentry_unref(dentry: parent_ref.get()); |
207 | |
208 | return child_ref; // the real dentry, or an error code |
209 | } |
210 | |
211 | static PtrResult<dentry_t> dentry_resolve_lastseg(dentry_t *parent, char *leaf, lastseg_resolve_flags_t flags, bool *is_symlink) |
212 | { |
213 | MOS_ASSERT(parent != NULL && leaf != NULL); |
214 | *is_symlink = false; |
215 | |
216 | pr_dinfo2(dcache, "resolving last segment: '%s'" , leaf); |
217 | const bool ends_with_slash = leaf[strlen(str: leaf) - 1] == PATH_DELIM; |
218 | if (ends_with_slash) |
219 | leaf[strlen(str: leaf) - 1] = '\0'; // remove the trailing slash |
220 | |
221 | if (unlikely(ends_with_slash && !(flags & RESOLVE_EXPECT_DIR))) |
222 | { |
223 | mos_warn("RESOLVE_EXPECT_DIR isn't set, but the provided path ends with a slash" ); |
224 | return -EINVAL; |
225 | } |
226 | |
227 | if (strncmp(str1: leaf, str2: "." , n: 2) == 0 || strcmp(str1: leaf, str2: "./" ) == 0) |
228 | return parent; |
229 | else if (strncmp(str1: leaf, str2: ".." , n: 3) == 0 || strcmp(str1: leaf, str2: "../" ) == 0) |
230 | { |
231 | if (parent == root_dentry) |
232 | return parent; |
233 | |
234 | dentry_t *const parent_parent = dentry_parent(dentry: *parent); |
235 | MOS_ASSERT(dentry_unref_one_norelease(parent)); // don't recursively unref all the way to the root |
236 | |
237 | // if the parent is a mountpoint, we need to jump to the mountpoint's parent |
238 | if (parent_parent->is_mountpoint) |
239 | return dentry_root_get_mountpoint(dentry: parent_parent); |
240 | |
241 | return parent_parent; |
242 | } |
243 | |
244 | auto child_ref = dentry_lookup_child(parent, name: leaf); // now we have a reference to the child |
245 | |
246 | if (unlikely(child_ref->inode == NULL)) |
247 | { |
248 | if (flags & RESOLVE_EXPECT_NONEXIST) |
249 | { |
250 | // do not use dentry_ref, because it checks for an inode |
251 | child_ref->refcount++; |
252 | return child_ref; |
253 | } |
254 | |
255 | pr_dinfo2(dcache, "file does not exist" ); |
256 | dentry_try_release(dentry: child_ref.get()); // child has no ref, we should release it directly |
257 | return -ENOENT; |
258 | } |
259 | |
260 | MOS_ASSERT(child_ref->refcount > 0); // dentry_get_child may return a negative dentry, which is handled above, otherwise we should have a reference on it |
261 | |
262 | if (flags & RESOLVE_EXPECT_NONEXIST && !(flags & RESOLVE_EXPECT_EXIST)) |
263 | { |
264 | dentry_unref(dentry: child_ref.get()); |
265 | return -EEXIST; |
266 | } |
267 | |
268 | if (child_ref->inode->type == FILE_TYPE_SYMLINK) |
269 | { |
270 | if (!(flags & RESOLVE_SYMLINK_NOFOLLOW)) |
271 | { |
272 | pr_dinfo2(dcache, "resolving symlink for '%s'" , leaf); |
273 | const auto symlink_target_ref = dentry_resolve_follow_symlink(d: child_ref.get(), flags); |
274 | // we don't need the symlink node anymore |
275 | MOS_ASSERT(dentry_unref_one_norelease(child_ref.get())); |
276 | *is_symlink = symlink_target_ref != nullptr; |
277 | return symlink_target_ref; |
278 | } |
279 | |
280 | pr_dinfo2(dcache, "not following symlink" ); |
281 | } |
282 | else if (child_ref->inode->type == FILE_TYPE_DIRECTORY) |
283 | { |
284 | if (!(flags & RESOLVE_EXPECT_DIR)) |
285 | { |
286 | MOS_ASSERT(dentry_unref_one_norelease(child_ref.get())); // it's the caller's responsibility to unref the parent and grandparents |
287 | return -EISDIR; |
288 | } |
289 | |
290 | // if the child is a mountpoint, we need to jump to the mounted filesystem's root |
291 | if (child_ref->is_mountpoint) |
292 | return dentry_ref(dentry: dentry_get_mount(dentry: child_ref.get())->root); |
293 | } |
294 | else |
295 | { |
296 | if (!(flags & RESOLVE_EXPECT_FILE)) |
297 | { |
298 | MOS_ASSERT(dentry_unref_one_norelease(child_ref.get())); // it's the caller's responsibility to unref the parent and grandparents |
299 | return -ENOTDIR; |
300 | } |
301 | } |
302 | |
303 | return child_ref; |
304 | } |
305 | |
306 | void dentry_attach(dentry_t *d, inode_t *inode) |
307 | { |
308 | MOS_ASSERT_X(d->inode == NULL, "reattaching an inode to a dentry" ); |
309 | MOS_ASSERT(inode != NULL); |
310 | // MOS_ASSERT_X(d->refcount == 1, "dentry %p refcount %zu is not 1", (void *) d, d->refcount); |
311 | |
312 | for (std::atomic_size_t i = 0; i < d->refcount; i++) |
313 | inode_ref(inode); // refcount the inode for each reference to the dentry |
314 | |
315 | inode_ref(inode); // refcount the inode for each reference to the dentry |
316 | d->inode = inode; |
317 | } |
318 | |
319 | void dentry_detach(dentry_t *d) |
320 | { |
321 | if (d->inode == NULL) |
322 | return; |
323 | |
324 | // the caller should have the only reference to the dentry |
325 | // MOS_ASSERT(d->refcount == 1); // !! TODO: this assertion fails in vfs_unlinkat |
326 | |
327 | (void) inode_unref(inode: d->inode); // we don't care if the inode is freed or not |
328 | d->inode = NULL; |
329 | } |
330 | |
331 | PtrResult<dentry_t> dentry_from_fd(fd_t fd) |
332 | { |
333 | if (fd == AT_FDCWD) |
334 | { |
335 | if (current_thread) |
336 | return current_process->working_directory; |
337 | else |
338 | return root_dentry; // no current process, so cwd is always root |
339 | } |
340 | |
341 | // sanity check: fd != AT_FDCWD, no current process |
342 | MOS_ASSERT(current_thread); |
343 | |
344 | io_t *io = process_get_fd(current_process, fd); |
345 | if (io == NULL) |
346 | return -EBADF; |
347 | |
348 | if (io->type != IO_FILE && io->type != IO_DIR) |
349 | return -EBADF; |
350 | |
351 | file_t *file = container_of(io, file_t, io); |
352 | return file->dentry; |
353 | } |
354 | |
355 | PtrResult<dentry_t> dentry_lookup_child(dentry_t *parent, const char *name) |
356 | { |
357 | if (unlikely(parent == nullptr)) |
358 | return nullptr; |
359 | |
360 | pr_dinfo2(dcache, "looking for dentry '%s' in '%s'" , name, dentry_name(parent).c_str()); |
361 | |
362 | // firstly check if it's in the cache |
363 | dentry_t *dentry = dentry_get_from_parent(sb: parent->superblock, parent, name); |
364 | MOS_ASSERT(dentry); |
365 | |
366 | spinlock_acquire(&dentry->lock); |
367 | |
368 | if (dentry->inode) |
369 | { |
370 | pr_dinfo2(dcache, "dentry '%s' found in the cache" , name); |
371 | spinlock_release(&dentry->lock); |
372 | return dentry_ref(dentry); |
373 | } |
374 | |
375 | // not in the cache, try to find it in the filesystem |
376 | if (parent->inode == NULL || parent->inode->ops == NULL || parent->inode->ops->lookup == NULL) |
377 | { |
378 | pr_dinfo2(dcache, "filesystem doesn't support lookup" ); |
379 | spinlock_release(&dentry->lock); |
380 | return dentry; |
381 | } |
382 | |
383 | const bool lookup_result = parent->inode->ops->lookup(parent->inode, dentry); |
384 | spinlock_release(&dentry->lock); |
385 | |
386 | if (lookup_result) |
387 | { |
388 | pr_dinfo2(dcache, "dentry '%s' found in the filesystem" , name); |
389 | return dentry_ref(dentry); |
390 | } |
391 | else |
392 | { |
393 | pr_dinfo2(dcache, "dentry '%s' not found in the filesystem" , name); |
394 | return dentry; // do not reference a negative dentry |
395 | } |
396 | } |
397 | |
398 | PtrResult<dentry_t> dentry_resolve(dentry_t *starting_dir, dentry_t *root_dir, const char *path, lastseg_resolve_flags_t flags) |
399 | { |
400 | if (!root_dir) |
401 | return -ENOENT; // no root directory |
402 | |
403 | char *last_segment; |
404 | pr_dinfo2(dcache, "resolving path '%s'" , path); |
405 | auto parent_ref = dentry_resolve_to_parent(base_dir: starting_dir, root_dir, original_path: path, last_seg_out: &last_segment); |
406 | if (parent_ref.isErr()) |
407 | { |
408 | pr_dinfo2(dcache, "failed to resolve parent of '%s', file not found" , path); |
409 | return parent_ref; |
410 | } |
411 | |
412 | if (last_segment == NULL) |
413 | { |
414 | // path is a single "/" |
415 | pr_dinfo2(dcache, "path '%s' is a single '/' or is empty" , path); |
416 | MOS_ASSERT(parent_ref == starting_dir); |
417 | if (!(flags & RESOLVE_EXPECT_DIR)) |
418 | { |
419 | dentry_unref(dentry: parent_ref.get()); |
420 | return -EISDIR; |
421 | } |
422 | |
423 | return parent_ref; |
424 | } |
425 | |
426 | bool symlink = false; |
427 | auto child_ref = dentry_resolve_lastseg(parent: parent_ref.get(), leaf: last_segment, flags, is_symlink: &symlink); |
428 | kfree(ptr: last_segment); |
429 | if (child_ref.isErr() || symlink) |
430 | dentry_unref(dentry: parent_ref.get()); // the lookup failed, or child_ref is irrelevant with the parent_ref |
431 | |
432 | return child_ref; |
433 | } |
434 | |
435 | static void dirter_add(vfs_listdir_state_t *state, u64 ino, mos::string_view name, file_type_t type) |
436 | { |
437 | vfs_listdir_entry_t *entry = mos::create<vfs_listdir_entry_t>(); |
438 | linked_list_init(list_node(entry)); |
439 | entry->ino = ino; |
440 | entry->name = name; |
441 | entry->type = type; |
442 | list_node_append(head: &state->entries, list_node(entry)); |
443 | state->n_count++; |
444 | } |
445 | |
446 | void vfs_populate_listdir_buf(dentry_t *dir, vfs_listdir_state_t *state) |
447 | { |
448 | // this call may not write all the entries, because the buffer may not be big enough |
449 | if (dir->inode->ops && dir->inode->ops->iterate_dir) |
450 | dir->inode->ops->iterate_dir(dir, state, dirter_add); |
451 | else |
452 | vfs_generic_iterate_dir(dir, state, op: dirter_add); |
453 | } |
454 | |