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