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
36static PtrResult<dentry_t> dentry_resolve_lastseg(dentry_t *parent, char *leaf, lastseg_resolve_flags_t flags, bool *symlink_resolved);
37static 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 */
48static 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
167static 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
211static 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
306void 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
319void 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
331PtrResult<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
355PtrResult<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
398PtrResult<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
435static 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
446void 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